TIP-3 Distributed token or “TON Cash”

Summary


A standard interface for distributed tokens.

Abstract


The following proposal defines the standard interface designed to implement fungible and non-fungible tokens in TON blockchain within smart contracts, as well as UTXO-like cash systems. This proposal provides basic functionality to mint and transfer tokens between token wallets.

Motivation


The standard interface allows minting tokens in TON blockchain and creating crypto wallets that can be used to transfer tokens between contracts. The suggested standard differs considerably from Ethereum ERC20 and other smart contract token standards with single registry due to its distributed nature related to TON blockchain particularities. Given that TON has a storage fee, using an existing ERC20 standard design would cause excessive maintenance costs. Also, ERC20 is somewhat incompatible with the sharding architecture. Therefore, a Distributed Token standard is preferable.

The ERC20 sharding implementation (with an idea to simply shard its registry) has drawbacks mainly related to complicated and expansive management. TIP-3 is fully distributed and implies separate storage of each user’s balance. Somewhat similar to UTXO design, it does not require sending a balance remaining after a transfer to another output. Anyway, the suggested design allows simulating UTXO if needed.

Specification


Glossary

Root Token Wallet (RTW) - a smart contract that mints token and deploys TON Token Wallets.

TON Token Wallet (TTW) - a smart contract deployed by RTW that stores some tokens; can send tokens to any other contract but can accept tokens only from other TTW contracts.

Root Public Key (RPK) - the public key of RTW that is a part of RTW initial data.

Wallet Public Key (WPK) - the public key of TTW that is a part of TTW initial data.

Token owner - an off-chain party that deploys RTW contract and owns the RTW keypair.

Token wallet owner - an off-chain party that owns the WPK keypair.

NFT - non-fungible token.

UTXO - unspent transaction output.

Description

Instead of a single classic ERC20 smart contract that stores all balances there are token wallets and each stores only one token balance. TTW accept messages with tokens only from other TTW contracts after a special verification check performed by TTW.

Root token wallet

The RTW is deployed by a token owner. The RTW stores TTW contract code. The token owner can ask the RTW to deploy a TTW contract with the defined WPK and to transfer some tokens to it. In that case the RTW sends an internal ‘constructor’ message with code and initial data of a TTW to the pre-calculated TTW address. Optionally, the message’s body can contain an encoded call to the grant function.

The RTW contains total amount of tokens (totalSupply), number of granted tokens (totalGranted) and optionally some token minting logic. (e.g. mint function).

TON token wallet

Initial data of a TTW contains 0 token balance, RPK, WPK and the RTW address.

A TTW contract can be deployed not only by the RTW contract. Anyone who has the TTW code, the RPK, and the RTW address can calculate the token wallet address and to deploy the wallet, but this TTW will have 0 tokens at the start.

When the ‘constructor’ message is received, the TTW can load the StateInit structure from the inbound message to store it in its persistent storage for later use. Otherwise, TTW can store TTW’s code as part of initial data.

The TTW contract receives external messages with transfer requests signed by the private key of token wallet keypair.

Note that the TTW cannot transfer more tokens than it has.

All TTW outbound internal messages must have the bounce flag set to true to prevent transferring tokens to undefined contracts or to receive tokens back in case of receiver contract error. If the TTW receives a bounced message (flag bounced == true), it must process it properly: decode the function ID from body and check if it is a token transfer function. If yes, then decode the token value from body and increase its own token balance by this value.

The TTW contract does not use setcode primitive and does not have function for arbitrary funds transfer except tokens.

Verification check

To verify that the sender contract is another token wallet, the TTW takes the following vertification steps:

  • loads the StateInit structure from persistent storage with the TTW code and initial data;
  • rewrites WPK in Data with sender WPK;
  • rebuilds the StateInit structure with TTW code and updated data;
  • calculates the representation hash from a cell where StateInit is placed;

Verification is succeeded if source address is equal to calculated hash. If not TTW should throw an exception.

After a verification, the TTW contract is sure that the sender contract is a TTW contract; now it must increase its token balance by value encoded in the inbound message body. Also, it can be sure that the sender decreases its token balance by the value of transferred tokens because it trusts its own code and it uses this code to calculate the sender address.

Gas

The sender TTW pays for gas used by the receiver TTW by attaching necessary amount of funds in value field of the internal message.

Token Protocol Diagram

UTXO extension

NOTE: For fungible token wallets.

UTXO-based wallet allows to identify tokens as coins. The set of all UTXOs represents total token supply. Each coin has an owner and amount of tokens it represents. So one coin is one wallet. This model allows, for example, to process transactions with coins in parallel.

UTXO can be implemented using current proposal for TON token wallet but with some additional features.

Allowance interface should be completely disabled for UTXO wallet.

A TTW cannot transfer tokens to an existing wallet.

A TTW can only receive tokens once, at the deploy message processing.

Every token transfer results in creation of at least 2 new wallets. The first holds the transferred amount of tokens and the second holds the remaining token balance. After the transfer the original wallet must have a zero token balance. Zero-balance destination wallets should not be created.

UTXO Root contract doesn’t have grant method, it can create wallets using deployWallet only.

Zero tokens in deployWallet should not be accepted.

To transfer tokens, a TTW has to perform the following steps within a single transaction:

  1. calculate the address of a new TTW #1 using the public key provided by the destination wallet owner.

    Note: see the **verification check** for the address calculation algorithm.

  2. deploy the new TTW #1 at the address calculated at step 1.

  3. send some tokens to it in an internal deploy (internalTransfer) message.

  4. calculate the address of a new TTW #2 using the new public key given by current wallet owner.

  5. deploy the new TTW #2 to the address calculated at step 4 and send the remaining tokens to it in a deploy (internalTransfer) message.

Initial data of a UTXO wallet contains an additional utxoFlag boolean flag set to false. When a wallet receives tokens, it is switched to true.

Functions


NOTE: All functions that can be called by external messages must check that an off-chain caller is a wallet owner by verifying a message body signature with the WPK.

TON Token wallet


There are 2 interfaces for fungible and non-fungible token wallets. Both of them must implement a Metadata interface.

Metadata interface


getName

OPTIONAL

Returns the name of the token, e.g. “MyToken”.

function getName() public (bytes name) (get-method)

getSymbol

OPTIONAL

Returns the token symbol, e.g. “GRM”.

function getSymbol() public (bytes symbol) (get-method)

getDecimals

OPTIONAL

Returns the number of decimals the token uses; e.g. 8, means to divide the token amount by 100,000,000 to get its user representation.

Examples:

token balance = 1500, decimals = 3, user representation = (1500 / 10^3) = 1.5 tokens.

token balance = 1500, decimals = 0, user representation = (1500 / 10^0) = 1500 tokens.

function getDecimals() public (uint8 decimals) (get-method)

getBalance

Returns number of tokens owned by a wallet.

function getBalance() public (uint256 balance) (get-method)

getWalletKey

Returns the wallet WPK.

function getWalletKey() public (uint256 walletKey) (get-method)

getRootAddress

Returns address of the root token wallet.

function getRootAddress() public (address rootAddress) (get-method)

Fungible interface


Wallet stores tokens as a uint256 integer in persistent data.

transfer

Called by an external message only.

Sends tokens to another token wallet. The function must call internalTransfer function of destination wallet.

NOTE: the function must complete successfully if the token balance is less than the transfer value.
NOTE: zero-value transfers must be treated as normal transfers. Transfer to zero address is not allowed.

// dest - Destination token wallet address.
// tokens - Number of tokens to transfer.
// grams - Number of nanograms to transfer with internal message. 
//         Must be enough to pay for gas used by destination wallet.
function transfer(address dest, uint256 tokens, uint128 grams) public void

internalTransfer

Called by an internal message only. Receives tokens from other token wallets. The function must NOT call accept or other buygas primitives.

NOTE: the function must do a verification check that sender is a TTW using senderKey.

// pubkey - WPK of sender contract.
// tokens - Amount of tokens to transfer.
function internalTransfer(uint256 senderKey, uint256 tokens) public void

accept

Called by an internal message only. Receives tokens from the RTW.

NOTE: the function must check that the message is sent by the RTW.

function accept(uint256 tokens) public void

approve

NOTE: see doc to understand why this interface is used https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit#

Called by an external message only.

Allows the spender wallet to withdraw tokens from the wallet multiple times, up to the tokens amount. If current spender allowance is equal to remainingTokens, then overwrite it with tokens, otherwise, do nothing.

function approve(address spender, uint256 remainingTokens, uint256 tokens) public 

allowance

Returns the amount of tokens the spender is still allowed to withdraw from the wallet.

function allowance() public (address spender, uint256 remainingTokens) (get-method)

transferFrom

Called by an external message only; allows transferring tokens from the dest wallet to to the wallet. The function must call the internalTransferFrom function of the dest contract and attach certain grams value to internal message.

NOTE: grams value must be enough to pay for 2 contract calls: internalTransferFrom function of dest and internalTransfer function of to.

function transferFrom(address dest, address to, uint256 tokens, uint128 grams) public

internalTransferFrom

Called by an internal message only; transfers the tokens amount from the wallet to the to contract.

The function must throw unless the message sender has a permission to withdraw such amount of tokens granted via the approve function. The function must decrease the allowed number of tokens by the tokens value and call the internalTransfer function of the to contract with tokens as an argument. The function must throw, if the current allowed amount of tokens is less than tokens.

NOTE: transfers of 0 values must be treated as normal transfers.

function internalTransferFrom(address to, uint256 tokens) public

Non-fungible interface


Wallet stores tokens as dictionary of token ids and token count as uint256 integer. Token Id is an uint256 integer.

transfer

Sends tokens to another token wallet; initiated by an external message only. The function must call the internalTransfer function of the destination wallet**.**

NOTE: The function must complete successfully, if a token with a defined ID is not owned by the wallet.
NOTE: Zero token Ids and transfers to zero addresses not allowed.

function transfer(address dest, uint256 tokenId, uint128 grams) public void

internalTransfer

Called by other token wallet contracts to send tokens. Initiated by an internal message only. The function MUST NOT call accept or other buygas primitives.

function internalTransfer(uint256 pubkey, uint256 tokenId) public void

accept

Allows receiving tokens from the RTW.

NOTE: the function must check that the message is sent by the RTW.

function accept(uint256 tokenId) public void

approve

Changes or reaffirms the approved token wallet for an NFT. An approved wallet can withdraw tokens by ID with the internalTransferFrom function.

function approve(address approved, uint256 tokenId) public

getTokenByIndex

Allows enumerating tokens owned by wallet. The function returns the token ID by its index.

function getTokenByIndex(uint256 index) public (uint256 tokenId) (get-method)

Index is ≥ 0 and < getBalance().

getApproved

Returns the approved address for a single NFT. The function returns zero if there is no approved address and throws exception if there is no token with tokenId.

function getApproved(uint256 tokenId) public (address approved) (get-method)

transferFrom

Called by an external message only; allows transferring tokens from the dest wallet to the to address. The function must call the internalTransferFrom function of dest .

NOTE: grams value must be enough to pay for 2 contract calls: internalTransferFrom function of dest and internalTransfer function of to.

function transferFrom(address dest, address to, uint256 tokenId, uint128 grams) public

internalTransferFrom

Called by an internal message only; transfers tokens with tokenId from the wallet to the to contract.

The function must throw unless the message sender has a permission to withdraw token granted via approve function. The function must remove token with tokenId from wallet and call internalTransfer function of the to contract with tokenId as argument.

The function must throw, if the message sender is not approved for token withdrawal with tokenId.

function internalTransferFrom(address to, uint256 tokenId) public

Extended interface

disapprove

Called by an external message only; cancels the permission to send tokens given to an approved wallet. The function must set the approved address and amount (or token Id) to 0.

function disapprove() public

Root Token Contract


When deployed, the RTW stores all tokens in the totalSupply variable and the number of granted tokens — totalGranted — is set to 0. totalGranted must be below or equal to totalSupply. Later, the RTW can mint more tokens.

Metadata interface

getName

Returns the name of the token - e.g. “MyToken”.

function getName() public bytes (get-method)

getSymbol

Returns the token symbol. E.g. “GRM”.

function getSymbol() public bytes (get-method)

getDecimals

Returns the number of decimals the token uses; e.g. 8, means to divide the token amount by 1,000,000,00 to get its user representation.

function getDecimals() public uint8 (get-method)

getRootKey

Returns the RTW public key.

function getRootKey() public (uint256 rootKey) (get-method)

getTotalSupply

Returns the total number of minted tokens.

function getTotalSupply() public (uint256 totalSupply) (get-method)

getWalletCode

Returns code of token wallet (as tree of cells).

function getWalletCode() public (cell walletCode) (get-method)

getWalletAddress

Calculates wallet address with defined public key.

NOTE: see **verification check** for details.

getWalletAddress(int8 workchainId, uint256 pubkey) public (address walletAddress) (get-method)

Root Token interface

deployWallet

Allows deploying the token wallet in a specified workchain and sending some tokens to it.

// Fungible version
function deployWallet(int8 workchainId, uint256 pubkey, uint256 tokens, uint128 grams) public (address walletAddress)
// Non-Fungible version
function deployWallet(int8 workchainId, uint256 pubkey, tokenId token, uint128 grams) public (address walletAddress)

grant

Called by an external message only; sends tokens to the TTW. The function must call the accept function of the token wallet and increase the totalGranted value.

// Fungible version
function grant(address dest, uint256 tokens, uint128 grams) public void 
// Non-fungible version
function grant(address dest, uint256 tokenId, uint128 grams) public void 

getTokenByIndex

Allows enumerating minted but not granted tokens in root wallet. The function returns the token ID by its index.

function getTokenByIndex(uint256 index) public (uint256 tokenId) (get-method)

mint

Called by an external message only; emits tokens and increases totalSupply.

NOTE: the logic about how to check that token with defined tokenId is already minted is not regulated in this specification. It is a matter of token implementation.

NOTE for NFT: the function returns id of minted token otherwise throws exception.

// Fungible version
function mint(uint256 tokens) public void
// Non-fungible version
function mint(uint256 tokenId) public (uint256 mintedId)

Implementation

// ==================================== Fungible ================================= //
struct allowance_info {
  lazy<MsgAddressInt> spender;
  TokensType remainingTokens;
};

// ===== TON Token wallet ===== //
__interface ITONTokenWallet {

  // expected offchain constructor execution
  __attribute__((internal, external, dyn_chain_parse))
  void constructor(bytes name, bytes symbol, uint8 decimals,
                   uint256 root_public_key, uint256 wallet_public_key,
                   lazy<MsgAddressInt> root_address, cell code) = 1;

  __attribute__((external, noaccept, dyn_chain_parse))
  void transfer(lazy<MsgAddressInt> dest, TokensType tokens, WalletGramsType grams) = 2;

  // Receive tokens from root
  __attribute__((internal, noaccept))
  void accept(TokensType tokens) = 3;

  // Receive tokens from other wallet
  __attribute__((internal, noaccept))
  void internalTransfer(TokensType tokens, uint256 pubkey) = 4;

  // getters
  __attribute__((getter))
  bytes getName() = 5;

  __attribute__((getter))
  bytes getSymbol() = 6;

  __attribute__((getter))
  uint8 getDecimals() = 7;

  __attribute__((getter))
  TokensType getBalance() = 8;

  __attribute__((getter))
  uint256 getWalletKey() = 9;

  __attribute__((getter))
  lazy<MsgAddressInt> getRootAddress() = 10;

  __attribute__((getter))
  allowance_info allowance() = 11;

  // allowance interface
  __attribute__((external, noaccept, dyn_chain_parse))
  void approve(lazy<MsgAddressInt> spender, TokensType remainingTokens, TokensType tokens) = 12;

  __attribute__((external, noaccept, dyn_chain_parse))
  void transferFrom(lazy<MsgAddressInt> dest, lazy<MsgAddressInt> to, TokensType tokens,
                    WalletGramsType grams) = 13;

  __attribute__((internal))
  void internalTransferFrom(lazy<MsgAddressInt> to, TokensType tokens) = 14;

  __attribute__((external, noaccept))
  void disapprove() = 15;
};

struct DTONTokenWallet {
  bytes name_;
  bytes symbol_;
  uint8 decimals_;
  TokensType balance_;
  uint256 root_public_key_;
  uint256 wallet_public_key_;
  lazy<MsgAddressInt> root_address_;
  cell code_;
  std::optional<allowance_info> allowance_;
};

struct ETONTokenWallet {
};

// ===== Root Token Contract ===== //
__interface IRootTokenContract {

  // expected offchain constructor execution
  __attribute__((internal, external, dyn_chain_parse))
  void constructor(bytes name, bytes symbol, uint8 decimals,
    uint256 root_public_key, cell wallet_code, TokensType total_supply) = 1;

  __attribute__((external, noaccept, dyn_chain_parse))
  lazy<MsgAddressInt> deployWallet(int8 workchain_id, uint256 pubkey, TokensType tokens, WalletGramsType grams) = 2;

  __attribute__((external, noaccept, dyn_chain_parse))
  void grant(lazy<MsgAddressInt> dest, TokensType tokens, WalletGramsType grams) = 3;

  __attribute__((external, noaccept))
  void mint(TokensType tokens) = 4;

  __attribute__((getter))
  bytes getName() = 5;

  __attribute__((getter))
  bytes getSymbol() = 6;

  __attribute__((getter))
  uint8 getDecimals() = 7;

  __attribute__((getter))
  uint256 getRootKey() = 8;

  __attribute__((getter))
  TokensType getTotalSupply() = 9;

  __attribute__((getter))
  TokensType getTotalGranted() = 10;

  __attribute__((getter))
  cell getWalletCode() = 11;

  __attribute__((getter))
  lazy<MsgAddressInt> getWalletAddress(int8 workchain_id, uint256 pubkey) = 12;
};

struct DRootTokenContract {
  bytes name_;
  bytes symbol_;
  uint8 decimals_;
  uint256 root_public_key_;
  TokensType total_supply_;
  TokensType total_granted_;
  cell wallet_code_;
};

struct ERootTokenContract {
};
// ==================================== UTXO ================================= //
// ===== TON Token wallet (UTXO) ===== //
__interface ITONTokenWallet {

  // expected offchain constructor execution
  __attribute__((internal, external, dyn_chain_parse))
  void constructor(bytes name, bytes symbol, uint8 decimals,
                   uint256 root_public_key, uint256 wallet_public_key,
                   lazy<MsgAddressInt> root_address, cell code) = 1;

  // tokens and grams_dest will be sent to a new deployed {workchain_dest, pubkey_dest} wallet
  //   and the rest of tokens and the rest of gas will be sent to new {workchain_rest, pubkey_rest} wallet
  __attribute__((external, noaccept, dyn_chain_parse))
  void transferUTXO(int8 workchain_dest, uint256 pubkey_dest, int8 workchain_rest, uint256 pubkey_rest,
                    TokensType tokens, WalletGramsType grams_dest) = 2;

  // Receive tokens from root
  __attribute__((internal, noaccept))
  void accept(TokensType tokens) = 3;

  // Receive tokens from other wallet
  __attribute__((internal, noaccept))
  void internalTransfer(TokensType tokens, uint256 pubkey) = 4;

  // getters
  __attribute__((getter))
  bytes getName() = 5;

  __attribute__((getter))
  bytes getSymbol() = 6;

  __attribute__((getter))
  uint8 getDecimals() = 7;

  __attribute__((getter))
  TokensType getBalance() = 8;

  __attribute__((getter))
  uint256 getWalletKey() = 9;

  __attribute__((getter))
  lazy<MsgAddressInt> getRootAddress() = 10;
};

struct DTONTokenWallet {
  bool_t utxo_received_;
  bytes name_;
  bytes symbol_;
  uint8 decimals_;
  TokensType balance_;
  uint256 root_public_key_;
  uint256 wallet_public_key_;
  lazy<MsgAddressInt> root_address_;
  cell code_;
};

struct ETONTokenWallet {
};

// ===== Root Token Contract (UTXO) ===== //
__interface IRootTokenContract {

  // expected offchain constructor execution
  __attribute__((internal, external, dyn_chain_parse))
  void constructor(bytes name, bytes symbol, uint8 decimals,
    uint256 root_public_key, cell wallet_code, TokensType total_supply) = 1;

  __attribute__((external, noaccept, dyn_chain_parse))
  lazy<MsgAddressInt> deployWallet(int8 workchain_id, uint256 pubkey, TokensType tokens, WalletGramsType grams) = 2;

  __attribute__((external, noaccept))
  void mint(TokensType tokens) = 3;

  __attribute__((getter))
  bytes getName() = 4;

  __attribute__((getter))
  bytes getSymbol() = 5;

  __attribute__((getter))
  uint8 getDecimals() = 6;

  __attribute__((getter))
  uint256 getRootKey() = 7;

  __attribute__((getter))
  TokensType getTotalSupply() = 8;

  __attribute__((getter))
  TokensType getTotalGranted() = 9;

  __attribute__((getter))
  cell getWalletCode() = 10;

  __attribute__((getter))
  lazy<MsgAddressInt> getWalletAddress(int8 workchain_id, uint256 pubkey) = 11;
};

struct DRootTokenContract {
  bytes name_;
  bytes symbol_;
  uint8 decimals_;
  uint256 root_public_key_;
  TokensType total_supply_;
  TokensType total_granted_;
  cell wallet_code_;
};

struct ERootTokenContract {
};
// ==================================== Non-fungible ================================= //
struct allowance_info {
  lazy<MsgAddressInt> spender;
  TokenId allowedToken;
};

// ===== TON Token wallet (Non-fungible) ===== //
__interface ITONTokenWallet {

  // expected offchain constructor execution
  __attribute__((internal, external, dyn_chain_parse))
  void constructor(bytes name, bytes symbol, uint8 decimals,
                   uint256 root_public_key, uint256 wallet_public_key,
                   lazy<MsgAddressInt> root_address, cell code) = 1;

  __attribute__((external, noaccept, dyn_chain_parse))
  void transfer(lazy<MsgAddressInt> dest, TokenId tokenId, WalletGramsType grams) = 2;

  // Receive tokens from root
  __attribute__((internal, noaccept))
  void accept(TokenId tokenId) = 3;

  // Receive tokens from other wallet
  __attribute__((internal, noaccept))
  void internalTransfer(TokenId tokenId, uint256 pubkey) = 4;

  // getters
  __attribute__((getter))
  bytes getName() = 5;

  __attribute__((getter))
  bytes getSymbol() = 6;

  __attribute__((getter))
  uint8 getDecimals() = 7;

  __attribute__((getter))
  TokensType getBalance() = 8;

  __attribute__((getter))
  uint256 getWalletKey() = 9;

  __attribute__((getter))
  lazy<MsgAddressInt> getRootAddress() = 10;

  __attribute__((getter))
  allowance_info allowance() = 11;

  __attribute__((getter))
  TokenId getTokenByIndex(TokensType index) = 12;

  __attribute__((getter))
  lazy<MsgAddressInt> getApproved(TokenId tokenId) = 13;

  // allowance interface
  __attribute__((external, noaccept, dyn_chain_parse))
  void approve(lazy<MsgAddressInt> spender, TokenId tokenId) = 14;

  __attribute__((external, noaccept, dyn_chain_parse))
  void transferFrom(lazy<MsgAddressInt> dest, lazy<MsgAddressInt> to, TokenId tokenId,
                    WalletGramsType grams) = 15;

  __attribute__((internal))
  void internalTransferFrom(lazy<MsgAddressInt> to, TokenId tokenId) = 16;

  __attribute__((external, noaccept))
  void disapprove() = 17;
};

struct DTONTokenWallet {
  bytes name_;
  bytes symbol_;
  uint8 decimals_;
  TokensType balance_;
  uint256 root_public_key_;
  uint256 wallet_public_key_;
  lazy<MsgAddressInt> root_address_;
  cell code_;
  std::optional<allowance_info> allowance_;
  dict_set<TokenId> tokens_;
};

struct ETONTokenWallet {
};

// ===== Root Token Contract (Non-fungible) ===== //
__interface IRootTokenContract {

  // expected offchain constructor execution
  __attribute__((internal, external, dyn_chain_parse))
  void constructor(bytes name, bytes symbol, uint8 decimals,
    uint256 root_public_key, cell wallet_code) = 1;

  __attribute__((external, noaccept, dyn_chain_parse))
  lazy<MsgAddressInt> deployWallet(int8 workchain_id, uint256 pubkey, TokenId tokenId, WalletGramsType grams) = 2;

  __attribute__((external, noaccept, dyn_chain_parse))
  void grant(lazy<MsgAddressInt> dest, TokenId tokenId, WalletGramsType grams) = 3;

  __attribute__((external, noaccept))
  TokenId mint(TokenId tokenId) = 4;

  __attribute__((getter))
  bytes getName() = 5;

  __attribute__((getter))
  bytes getSymbol() = 6;

  __attribute__((getter))
  uint8 getDecimals() = 7;

  __attribute__((getter))
  uint256 getRootKey() = 8;

  __attribute__((getter))
  TokensType getTotalSupply() = 9;

  __attribute__((getter))
  TokensType getTotalGranted() = 10;

  __attribute__((getter))
  cell getWalletCode() = 11;
  
  __attribute__((getter))
  TokenId getLastMintedToken() = 12

  __attribute__((getter))
  lazy<MsgAddressInt> getWalletAddress(int8 workchain_id, uint256 pubkey) = 13;
};

struct DRootTokenContract {
  bytes name_;
  bytes symbol_;
  uint8 decimals_;
  uint256 root_public_key_;
  TokensType total_supply_;
  TokensType total_granted_;
  cell wallet_code_;
  TokenId last_minted_token_;
  dict_set<TokenId> tokens_;
};

struct ERootTokenContract {
};

Limitations:

It is impossible to provide “ownerOf” function

function ownerOf(uint256 _tokenId) external view returns (address);

for non fungible tokens since there is no single registry of such tokens. It is impossible to recognise from its ID who it belongs to.

Note: we believe “it’s a feature, not a limitation”.

17 Likes

As I understand there is no way to update TTW contract:

  • each account has its own copy of TTW
  • TTW checks a remote side
  • the check loads the StateInit structure, do some manipulations and calculate hash, and if hash is other that it knows, it doesn’t trust to the remote side
  • its impossible to update a copy of TTW, because they will not trust each other
  • its impossible to update each copy of TTW, because a huge number of them
  • its impossible to update because it is a some delay between the verification check and a transfer, and it’s possible that contract can update in this time

So, in case of error in the TTW, we can’t update it. And we must make good audit of RTW-TTW contract before launch.

Do I understand correctly?

1 Like

That is correct. The contract code should be the same. We definitely need to have it audited and formally verified. As I believe all important contracts which holds value should.

1 Like

Great work!

I want to clarify, how fungible “TON Cash” related to “CurrencyCollection”? I found mentions at docs ton dev and at TON Blockchain description 3.1.6.

I understand correctly that User has to create new wallet for each new token and care about their balances in native tokens to pay fees?

How many fee required to pass all verification steps to confirm that TTW the same?

TON Cash has no connection with Currency Collection. The purpose is different as well as properties. May be we should create another post about Currency Collections,how they work and so on.

The user or the Root Contract creates wallets for each token for a particular user.

As for the fees it goes like that:

wallet transfer gas used: 15585 to send
Gas Used (9.58%) 11 496 to accept

Example of child transaction for reciever: https://net.ton.live/transactions?section=details&id=f4b51d60b6da92e83d9a0128156fef64c3fe4ca6c1445390490f94c54952aee0

For sender: https://net.ton.live/transactions?section=details&id=da174909c45d24d187702760e17d3c0e14b5d77bdfebb206f90ab0711bcbaf41

Gas Used 15 585

In total in TONs 0,015585 + 0,011496 = 0,027081‬ T for both

1 Like

It is very good that you describe all these commands. Since many people who wish may have a problem with starting nodes due to a simple lack of knowledge of what and how. I advise you to translate for many into different languages, as well as make a separate video with step-by-step instructions.

1 Like

Storing info on Free TON blockchain cost money. So if Token Wallet runs out of gas it will disappear. Thus tokens may simply disappear, because Token Wallet with them gets deleted. Is this true?

There is an unfreez mechanism.

  1. Who send :gem: to TTW to pay storage fee? And I find tvc_accept in TTW. Who send :gem: to pay for it?
  2. How unfreeze mechanism woks?

Anyone can send some tokens to any of these addresses to pay the fees. So if you have your Wallet you should have some small amount of TONs there to pay for storage fees (as with any address in TON, no difference between TIP3 or native currency)

Root Contract Owner is paying to Root for deployment of the Token wallet to a user. So he is paying or a user should send some tokens to owner for him to deploy user Wallet. Or anyone can just send some tokens to Root but of course there is no guarantee the Root will spend it on some particular purpose.

We are thinking about adding an internal Wallet deploy so user can pay for this but its not yet implemented.

This does not belong to this topic. You need to send a special message with account state before it was frozen. There is an undocumented feature for account unreeze in TONOS-CLI. Will publish it sometimes :slight_smile:

1 Like

Greetings to all.

While working on tokens for the TON-Ethereum Bridge and further embedding into the wallet application, our team faced a number of practical problems that required adding new mechanics to token contracts and deviating from the standard described above in some places.

The result was this Solidity implementation of fungible tokens.

The link below contains proposals for amending / supplementing the standard and a number of topics requiring joint discussion. Please pay special attention to the 2nd and 5th points.

11 Likes

Small proposals regarding allowance in TIP-3 (both FT and NFT):

  1. [BOTH] Check for spender zero address;
  2. [FT] Either change allowance function from void to bool to return false on fail (void approve(...) => bool approve(...)) or change the following code:
    if (allowance_->remainingTokens == remainingTokens)
    to
    require(allowance_->remainingTokens == remainingTokens, ERROR_CODE)
    to know when we failed to change allowance.
  3. [BOTH] Currently allowance is a local variable and it prevents TTW owner to give allowance to multiple contracts. Change allowance from local variable to mapping to allow multiple allowances at the same time (like it’s made in ERC20).

Thank you.

How do we associate data with the NFT token, how do we store a file or json object on the chain and associate it with the NFT token?

As far as I understand the standard does not cover the storage of the object itself. Just its id or may be its hash. The object can be stored somewhere off-chain.

Where can I see the complete TIP3.1 token standard,
Contains transfer and accept method examples?