In general, inifinite data structures that can actually grow to billions of elements are very difficult to implement on blockchain. As the contract persistent state grows in size, read and write operations become more expensive in gas. In the extreme, they may cost more than a transaction gas limit, rendering the contract unusable.
It is therefore important to design contracts to have an upper bound on state size. If so, how would we implement a token with a map of balances that can scale to billions of holders?
The secret of infinite scalability on TON is sharding the data across multiple contracts. We can apply the parent-child design pattern to do just this.
In this example, we hold the balance of every holder in a separate child contract.
To transfer tokens, the owner sends the Transfer message to the child contract holding their own balance. This will cause the child to deploy its sibling - the child contract holding the recipient's balance - by sending it the InternalAddTokens message.
This example also handles gas efficiently. The excess gas from every operation is refunded to the original sender.
All Examplesimport "@stdlib/ownable";
message Mint {
amount: Int;
receiver: Address;
}
contract SampleJetton with Jetton {
totalSupply: Int as coins;
owner: Address;
content: Cell;
mintable: Bool;
max_supply: Int as coins;
init(owner: Address, content: Cell, max_supply: Int) {
self.totalSupply = 0;
self.owner = owner;
self.mintable = true;
self.content = content;
self.max_supply = max_supply; // Initial Setting for max_supply
}
receive(msg: Mint) {
let ctx: Context = context();
require(ctx.sender == self.owner, "Not Owner");
require(self.mintable, "Can't Mint Anymore");
self.mint(msg.receiver, msg.amount, self.owner); // (to, amount, response_destination)
}
receive("Mint: 100") { // Public Minting
let ctx: Context = context();
require(self.mintable, "Can't Mint Anymore");
self.mint(ctx.sender, 100, self.owner);
}
receive("Owner: MintClose") {
let ctx: Context = context();
require(ctx.sender == self.owner, "Not Owner");
self.mintable = false;
}
}
struct JettonData {
totalSupply: Int;
mintable: Bool;
owner: Address;
content: Cell;
walletCode: Cell;
}
// ============================================================================================================ //
@interface("org.ton.jetton.master")
trait Jetton with Ownable {
totalSupply: Int; // Already set initially
mintable: Bool;
owner: Address;
content: Cell;
max_supply: Int; // This is not in the TEP-74 interface
receive(msg: TokenUpdateContent) {
self.requireOwner(); // Allow changing content only by owner
self.content = msg.content; // Update content
}
receive(msg: TokenBurnNotification) {
self.requireWallet(msg.owner); // Check wallet
self.totalSupply = self.totalSupply - msg.amount; // Update supply
if (msg.response_destination != null) { // Cashback
send(SendParameters{
to: msg.response_destination!!,
value: 0,
bounce: false,
mode: SendRemainingValue + SendIgnoreErrors,
body: TokenExcesses{
queryId: msg.queryId
}.toCell()
});
}
}
// @to The Address receive the Jetton token after minting
// @amount The amount of Jetton token being minted
// @response_destination The previous owner address
fun mint(to: Address, amount: Int, response_destination: Address) {
require(self.totalSupply + amount <= self.max_supply, "The total supply will be overlapping.");
self.totalSupply = self.totalSupply + amount; // Update total supply
let winit: StateInit = self.getJettonWalletInit(to); // Create message
send(SendParameters{
to: contractAddress(winit),
value: 0,
bounce: false,
mode: SendRemainingValue,
body: TokenTransferInternal{
queryId: 0,
amount: amount,
from: myAddress(),
response_destination: response_destination,
forward_ton_amount: 0,
forward_payload: emptySlice()
}.toCell(),
code: winit.code,
data: winit.data
});
}
fun requireWallet(owner: Address) {
let ctx: Context = context();
let winit: StateInit = self.getJettonWalletInit(owner);
require(contractAddress(winit) == ctx.sender, "Invalid sender");
}
virtual fun getJettonWalletInit(address: Address): StateInit {
return initOf JettonDefaultWallet(myAddress(), address);
}
// ====== Get Methods ====== //
get fun get_jetton_data(): JettonData {
let code: Cell = self.getJettonWalletInit(myAddress()).code;
return JettonData{
totalSupply: self.totalSupply,
mintable: self.mintable,
owner: self.owner,
content: self.content,
walletCode: code
};
}
get fun get_wallet_address(owner: Address): Address {
let winit: StateInit = self.getJettonWalletInit(owner);
return contractAddress(winit);
}
}
// ============================================================ //
@interface("org.ton.jetton.wallet")
contract JettonDefaultWallet {
const minTonsForStorage: Int = ton("0.01");
const gasConsumption: Int = ton("0.01");
balance: Int;
owner: Address;
master: Address;
init(master: Address, owner: Address) {
self.balance = 0;
self.owner = owner;
self.master = master;
}
receive(msg: TokenTransfer) { // 0xf8a7ea5
let ctx: Context = context(); // Check sender
require(ctx.sender == self.owner, "Invalid sender");
// Gas checks
let fwdFee: Int = ctx.readForwardFee() + ctx.readForwardFee();
let final: Int = 2 * self.gasConsumption + self.minTonsForStorage + fwdFee;
require(ctx.value > min(final, ton("0.01")), "Invalid value!!");
// Update balance
self.balance = self.balance - msg.amount;
require(self.balance >= 0, "Invalid balance");
let init: StateInit = initOf JettonDefaultWallet(self.master, msg.destination);
let walletAddress: Address = contractAddress(init);
send(SendParameters{
to: walletAddress,
value: 0,
mode: SendRemainingValue,
bounce: false,
body: TokenTransferInternal{
queryId: msg.queryId,
amount: msg.amount,
from: self.owner,
response_destination: msg.response_destination,
forward_ton_amount: msg.forward_ton_amount,
forward_payload: msg.forward_payload
}.toCell(),
code: init.code,
data: init.data
});
}
receive(msg: TokenTransferInternal) { // 0x178d4519
let ctx: Context = context();
if (ctx.sender != self.master) {
let sinit: StateInit = initOf JettonDefaultWallet(self.master, msg.from);
require(contractAddress(sinit) == ctx.sender, "Invalid sender!");
}
// Update balance
self.balance = self.balance + msg.amount;
require(self.balance >= 0, "Invalid balance");
// Get value for gas
let msgValue: Int = self.msgValue(ctx.value);
let fwdFee: Int = ctx.readForwardFee();
msgValue = msgValue - msg.forward_ton_amount - fwdFee;
// 0x7362d09c - notify the new owner of JettonToken that the transfer is complete
if (msg.forward_ton_amount > 0) {
send(SendParameters{
to: self.owner,
value: msg.forward_ton_amount,
mode: SendPayGasSeparately + SendIgnoreErrors,
bounce: false,
body: TokenNotification {
queryId: msg.queryId,
amount: msg.amount,
from: msg.from,
forward_payload: msg.forward_payload
}.toCell()
});
}
// 0xd53276db -- Cashback to the original Sender
if (msg.response_destination != null) {
send(SendParameters {
to: msg.response_destination,
value: msgValue,
bounce: false,
body: TokenExcesses {
queryId: msg.queryId
}.toCell(),
mode: SendIgnoreErrors
});
}
}
receive(msg: TokenBurn) {
let ctx: Context = context();
require(ctx.sender == self.owner, "Invalid sender"); // Check sender
self.balance = self.balance - msg.amount; // Update balance
require(self.balance >= 0, "Invalid balance");
let fwdFee: Int = ctx.readForwardFee(); // Gas checks
require(ctx.value > fwdFee + 2 * self.gasConsumption + self.minTonsForStorage, "Invalid value - Burn");
// Burn tokens
send(SendParameters{
to: self.master,
value: 0,
mode: SendRemainingValue,
bounce: true,
body: TokenBurnNotification{
queryId: msg.queryId,
amount: msg.amount,
owner: self.owner,
response_destination: self.owner
}.toCell()
});
}
get fun msgValue(value: Int): Int {
let msgValue: Int = value;
let tonBalanceBeforeMsg: Int = myBalance() - msgValue;
let storageFee: Int = self.minTonsForStorage - min(tonBalanceBeforeMsg, self.minTonsForStorage);
msgValue = msgValue - (storageFee + self.gasConsumption);
return msgValue;
}
bounced(src: bounced) {
self.balance = self.balance + src.amount;
}
bounced(src: bounced) {
self.balance = self.balance + src.amount;
}
get fun get_wallet_data(): JettonWalletData {
return JettonWalletData{
balance: self.balance,
owner: self.owner,
master: self.master,
walletCode: (initOf JettonDefaultWallet(self.master, self.owner)).code
};
}
}
struct JettonWalletData {
balance: Int;
owner: Address;
master: Address;
walletCode: Cell;
}
message(0xf8a7ea5) TokenTransfer {
queryId: Int as uint64;
amount: Int as coins;
destination: Address;
response_destination: Address;
custom_payload: Cell?;
forward_ton_amount: Int as coins;
forward_payload: Slice as remaining; // Comment Text message when Transfer the jetton
}
message(0x178d4519) TokenTransferInternal {
queryId: Int as uint64;
amount: Int as coins;
from: Address;
response_destination: Address;
forward_ton_amount: Int as coins;
forward_payload: Slice as remaining; // Comment Text message when Transfer the jetton
}
message(0x7362d09c) TokenNotification {
queryId: Int as uint64;
amount: Int as coins;
from: Address;
forward_payload: Slice as remaining; // Comment Text message when Transfer the jetton
}
message(0x595f07bc) TokenBurn {
queryId: Int as uint64;
amount: Int as coins;
owner: Address;
response_destination: Address;
}
message(0x7bdd97de) TokenBurnNotification {
queryId: Int as uint64;
amount: Int as coins;
owner: Address;
response_destination: Address?;
}
message(0xd53276db) TokenExcesses {
queryId: Int as uint64;
}
message TokenUpdateContent {
content: Cell;
}