├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── contracts ├── airdrop.fc ├── airdrop_helper.fc ├── constants.fc ├── imports │ └── stdlib.fc ├── jetton │ ├── jetton-utils.fc │ ├── jetton_minter.fc │ ├── jetton_wallet.fc │ ├── op-codes.fc │ └── params.fc └── scheme.tlb ├── jest.config.ts ├── package.json ├── scripts ├── claimAirdrop.ts └── deployAirdrop.ts ├── tests └── Airdrop.spec.ts ├── tsconfig.json ├── wrappers ├── Airdrop.compile.ts ├── Airdrop.ts ├── AirdropHelper.compile.ts ├── AirdropHelper.ts ├── JettonMinter.compile.ts ├── JettonMinter.ts ├── JettonWallet.compile.ts └── JettonWallet.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | temp 3 | build 4 | dist 5 | .DS_Store -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "singleQuote": true, 5 | "bracketSpacing": true, 6 | "semi": true 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Ton Raffles 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scalable Airdrop System with date start 2 | 3 | This repository contains an implementation of a Scalable Airdrop System for the TON blockchain. It can be used to distribute Jettons on-chain to any number of wallets. 4 | 5 | ## Table of contents 6 | 7 | - [Technical description](#technical-description) 8 | - [Motivation](#motivation) 9 | - [Solution](#solution) 10 | - [Possible improvements](#possible-improvements) 11 | - [Documentation](#documentation) 12 | - [Preparing the list of entries](#preparing-the-list-of-entries) 13 | - [Deploying the Airdrop](#deploying-the-airdrop) 14 | - [Claiming the Airdrop](#claiming-the-airdrop) 15 | 16 | ## Technical description 17 | 18 | ### Motivation 19 | 20 | We can classify old on-chain Airdrop systems into three groups based on their mechanism: 21 | 22 | - Distribution of all tokens at once: The distributor pays all the fees. This is simple and fast, making it a good approach for small Airdrops (<1000 recipients). However, it requires you to pay the fees for distributing all the tokens. For large Airdrops, you will end up spending thousands of dollars just for fees. 23 | - Storing all recipients in contract storage and distributing tokens individually to each user: Users pay fees for claiming tokens. This approach requires users to send a claiming message to receive their tokens. You do not spend a lot of money on fees with this approach, but the smart contract stores tens of thousands of entries in an on-chain dictionary, which is also very costly and inefficient. 24 | - Storing Airdrop entries in a Merkle tree: The smart contract stores the root hash of the tree and a list of addresses that have already claimed the Airdrop. This approach eliminates the need to pay high fees during deployment but can still be costly for users on a large scale. 25 | 26 | The system described in this repository is scalable, inexpensive, and maintains a consistent claiming process for users. 27 | 28 | ### Solution 29 | 30 | We will store the list of Airdrop entries in a Merkle tree, which allows us to quickly and cheaply check whether any record belongs to the original list, similar to the third group of Airdrops described earlier. This tree can be stored off-chain, while the smart contract only needs to store the root hash of the tree. 31 | 32 | Each record can look like this: 33 | 34 | ``` 35 | _ address:MsgAddressInt amount:Coins = AirdropEntry; 36 | ``` 37 | 38 | To claim the Airdrop, the user only needs to provide the Merkle proof of their entry. 39 | 40 | Since we do not want to dynamically change the tree during the contract process and do not want to add unpredictable and large fees for users, we need another way to protect against repeated claims. Distributed architecture of TON allows us to easily add another contract to the system - `AirdropHelper`. Its only job is to act as a boolean variable that shows whether the user has already claimed tokens or not. This contract will store the recipient and main `Airdrop` contract addresses in its data so that each user has a separate contract with deterministic address that can be calculated within the main contract. 41 | 42 | Upon deployment, the `AirdropHelper` contract checks if the deployer is the main `Airdrop` contract and sends a message indicating success back. For all future calls, this contract will throw an error because it has been deployed previously. Such a contract does not require a lot of coins on its balance to stay alive for a long time. **0.05 TON** would be enough for many years, which is more than sufficient. 43 | 44 | The main advantage of using separate contracts here is that the claiming fees are the same for all users, regardless of the number of users who have already claimed tokens. 45 | 46 | ### Possible improvements 47 | 48 | With the simple and generic logic of this system, we can easily modify it to work not only with Jetton Airdrops but also with arbitrary messages. The smart contract can be used to send arbitrary messages on its behalf upon requests from external sources while ensuring that each message will be sent only once. 49 | 50 | For example, this system could be adapted not only for Airdrops of Jettons that follow the TEP-74 standard but also for NFTs or any other possible future standards and modifications of tokens. 51 | 52 | ## Documentation 53 | 54 | ### Important Consideration 55 | 56 | Currently, most wallet applications have a limitation: they do not support the sending of exotic cells. Due to this, the present implementation relies on external messages for transmitting a Merkle proofs. As a result, the process to claim an airdrop involves a two-step procedure: 57 | 58 | 1. Send an internal deployment message, excluding the Merkle proof cell. 59 | 2. Send an external message containing the Merkle proof. 60 | 61 | This method may seem less streamlined and may pose challenges for both developers and users. However, given the current constraints, it is the most viable approach to operate this smart contract system. 62 | 63 | > :warning: As soon as wallet applications support the sending of exotic cells, all smart contracts will undergo a rework to enhance their efficiency and usability. 64 | 65 | ### Preparing the list of entries 66 | 67 | The first thing you have to do is to prepare a list of Airdrop entries. Each item of that list contains an address of the receiver and the amount of Jettons they can receive. 68 | 69 | Example: 70 | 71 | ``` 72 | EQBKgXCNLPexWhs2L79kiARR1phGH1LwXxRbNsCFF9doc2lN: 100 73 | EQDxC1erzS2fub_CNmkdH1A3hRs6xMDrWBmOD2yQOZjRuruv: 200 74 | EQB4cwGljhouzFwc6EHpCacCtsK7_XIj-tNfM5udgW6IxO9R: 150 75 | ``` 76 | 77 | Smart contract works with this list in a form of a Dictionary (`Hashmap` TL-B structure). Here is how you can generate it from such a list. 78 | 79 | ```ts 80 | const entries: AirdropEntry[] = [ 81 | { 82 | address: Address.parse('EQBKgXCNLPexWhs2L79kiARR1phGH1LwXxRbNsCFF9doc2lN'), 83 | amount: toNano('100'), 84 | }, 85 | { 86 | address: Address.parse('EQDxC1erzS2fub_CNmkdH1A3hRs6xMDrWBmOD2yQOZjRuruv'), 87 | amount: toNano('200'), 88 | }, 89 | { 90 | address: Address.parse('EQB4cwGljhouzFwc6EHpCacCtsK7_XIj-tNfM5udgW6IxO9R'), 91 | amount: toNano('150'), 92 | }, 93 | ]; 94 | 95 | const dict = generateEntriesDictionary(entries); 96 | const dictCell = beginCell().storeDictDirect(dict).endCell(); 97 | const merkleRoot = BigInt('0x' + dictCell.hash().toString('hex')); 98 | ``` 99 | 100 | You will need the `dict` and the `merkleRoot` values to deploy and use the Airdrop smart contract. 101 | 102 | ### Deploying the Airdrop 103 | 104 | You can use ready script from [scripts/](/scripts/deployAirdrop.ts) directory of this repository. 105 | Here is an example. 106 | 107 | ```ts 108 | const airdrop = provider.open( 109 | Airdrop.createFromConfig( 110 | { 111 | merkleRoot, 112 | helperCode: await compile('AirdropHelper'), 113 | }, 114 | await compile('Airdrop') 115 | ) 116 | ); 117 | 118 | await airdrop.sendDeploy(provider.sender(), toNano('0.05'), await jettonMinter.getWalletAddressOf(airdrop.address)); 119 | ``` 120 | 121 | After the transaction is succesfully sent and confirmed on-chain, your Airdrop will become available to use. 122 | 123 | Please remember, that in order to let the Airdrop contract send Jettons to their receivers, you need to transfer the required amount of them to it. 124 | Simply transfer the required amount (sum of all entries) to the `airdrop.address` and it will work as intended. 125 | 126 | ### Claiming the Airdrop 127 | 128 | In order to claim an Airdrop, you need to call `sendClaim` method of `airdropHelper` smart contract which you should deploy. 129 | 130 | These calls can easily be integrated into front-end of your website or into some Telegram bot. 131 | 132 | ```ts 133 | // suppose that you have the cell in base64 form stored somewhere 134 | const dictCell = Cell.fromBase64( 135 | 'te6cckEBBQEAhgACA8/oAgEATUgA8OYDSxw0XZi4OdCD0hNOBW2Fd/rkR/Wmvmc3OwLdEYiLLQXgEAIBIAQDAE0gAkQn3LTRp9vn/K0TXJrWPCeEmrX7VdoMP2KoakM4TmSaO5rKAEAATSACVAuEaWe9itDZsX37JEAijrTCMPqXgvii2bYEKL67Q5odzWUAQC6Eo5U=' 136 | ); 137 | const dict = dictCell.beginParse().loadDictDirect(Dictionary.Keys.BigUint(256), airdropEntryValue); 138 | 139 | const entryIndex = 123n; 140 | 141 | const proof = dict.generateMerkleProof(entryIndex); 142 | 143 | const helper = provider.open( 144 | AirdropHelper.createFromConfig( 145 | { 146 | airdrop: Address.parse('EQAGUXoAPHIHYleSbSE05egNAlK8YAaYqUQsMho709gMBXU2'), 147 | index: entryIndex, 148 | proofHash: proof.hash(), 149 | }, 150 | await compile('AirdropHelper') 151 | ) 152 | ); 153 | 154 | if (!(await provider.isContractDeployed(helper.address))) { 155 | await helper.sendDeploy(provider.sender()); 156 | await provider.waitForDeploy(helper.address); 157 | } 158 | 159 | await helper.sendClaim(0n, proof); 160 | ``` 161 | 162 | The cell containing dictionary must be stored in some reliable place. It can be your own server, some cloud storage or even TON Storage. 163 | Make sure to not lose both original list and the cell dictionary at the same time because you will not be able to recover them. 164 | 165 | 166 | ## License 167 | This project is licensed under the MIT License. See the LICENSE file for details. 168 | -------------------------------------------------------------------------------- /contracts/airdrop.fc: -------------------------------------------------------------------------------- 1 | #include "imports/stdlib.fc"; 2 | #include "jetton/jetton-utils.fc"; 3 | #include "constants.fc"; 4 | 5 | global slice data::jetton_wallet; 6 | global int data::merkle_root; 7 | global cell data::helper_code; 8 | global int data::begin; 9 | global slice data::admin; 10 | 11 | global int context::op; 12 | global slice context::sender; 13 | global int context::query_id; 14 | 15 | () load_data() impure inline { 16 | slice ds = get_data().begin_parse(); 17 | data::jetton_wallet = ds~load_msg_addr(); 18 | data::merkle_root = ds~load_uint(256); 19 | data::helper_code = ds~load_ref(); 20 | data::begin = ds~load_uint(64); 21 | data::admin = ds~load_msg_addr(); 22 | } 23 | 24 | () save_data() impure inline { 25 | set_data(begin_cell() 26 | .store_slice(data::jetton_wallet) 27 | .store_uint(data::merkle_root, 256) 28 | .store_ref(data::helper_code) 29 | .store_uint(data::begin, 64) 30 | .store_slice(data::admin) 31 | .end_cell()); 32 | } 33 | 34 | (slice, int) begin_parse_exotic(cell c) asm "XCTOS"; 35 | 36 | (cell) helper_stateinit(int proof_hash, int index) { 37 | return begin_cell() 38 | .store_uint(6, 5) 39 | .store_ref(data::helper_code) 40 | .store_ref(begin_cell() 41 | .store_uint(0, 1) 42 | .store_slice(my_address()) 43 | .store_uint(proof_hash, 256) 44 | .store_uint(index, 256) 45 | .end_cell()) 46 | .end_cell(); 47 | } 48 | 49 | (slice) helper_address(cell stateinit) { 50 | return begin_cell() 51 | .store_uint(0x400, 11) 52 | .store_uint(cell_hash(stateinit), 256) 53 | .end_cell().begin_parse(); 54 | } 55 | 56 | () send_tokens(slice recipient, int amount) impure { 57 | send_raw_message(begin_cell() 58 | .store_uint(0x18, 6) 59 | .store_slice(data::jetton_wallet) 60 | .store_coins(0) 61 | .store_uint(1, 107) 62 | .store_ref(begin_cell() 63 | .store_uint(op::jetton::transfer, 32) 64 | .store_uint(context::query_id, 64) 65 | .store_coins(amount) 66 | .store_slice(recipient) 67 | .store_slice(recipient) 68 | .store_uint(0, 1) 69 | .store_coins(10000000) 70 | .store_uint(0, 1) 71 | .end_cell()) 72 | .end_cell(), 64); 73 | } 74 | 75 | () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { 76 | if (in_msg_body.slice_bits() < 96) { 77 | return (); 78 | } 79 | 80 | context::op = in_msg_body~load_uint(32); 81 | context::query_id = in_msg_body~load_uint(64); 82 | (_, context::sender) = in_msg_full.begin_parse().skip_bits(4).load_msg_addr(); 83 | 84 | load_data(); 85 | 86 | if (context::op == op::deploy) { 87 | throw_unless(error::already_deployed, data::jetton_wallet.preload_uint(2) == 0); 88 | data::jetton_wallet = in_msg_body~load_msg_addr(); 89 | save_data(); 90 | } 91 | 92 | elseif (context::op == op::process_claim) { 93 | throw_unless(error::wrong_time, now() >= data::begin); 94 | 95 | cell proof_cell = in_msg_body~load_ref(); 96 | int index = in_msg_body~load_uint(256); 97 | 98 | (slice cs, int exotic?) = proof_cell.begin_parse_exotic(); 99 | throw_unless(42, exotic?); 100 | throw_unless(43, cs~load_uint(8) == 3); 101 | throw_unless(44, data::merkle_root == cs~load_uint(256)); 102 | 103 | cell dict = cs~load_ref(); 104 | (slice entry, int found?) = dict.udict_get?(256, index); 105 | throw_unless(45, found?); 106 | 107 | throw_unless(error::wrong_sender, equal_slices(context::sender, helper_address(helper_stateinit(proof_cell.cell_hash(), index)))); 108 | 109 | send_tokens(entry~load_msg_addr(), entry~load_coins()); 110 | } 111 | 112 | elseif (context::op == op::withdraw_jettons) { 113 | throw_unless(error::wrong_sender, equal_slices(context::sender, data::admin)); 114 | throw_unless(error::wrong_time, now() < data::begin); 115 | int amount = in_msg_body~load_coins(); 116 | send_tokens(context::sender, amount); 117 | } 118 | 119 | else { 120 | throw(0xffff); 121 | } 122 | } 123 | 124 | (slice, int, cell, int) get_contract_data() method_id { 125 | load_data(); 126 | return (data::jetton_wallet, data::merkle_root, data::helper_code, data::begin); 127 | } 128 | -------------------------------------------------------------------------------- /contracts/airdrop_helper.fc: -------------------------------------------------------------------------------- 1 | #include "imports/stdlib.fc"; 2 | #include "constants.fc"; 3 | 4 | () set_claimed(int claimed) impure { 5 | set_data(begin_cell() 6 | .store_int(claimed, 1) 7 | .store_slice(get_data().begin_parse().skip_bits(1)) 8 | .end_cell()); 9 | } 10 | 11 | () recv_internal(cell in_msg_full, slice in_msg_body) impure { 12 | slice cs = in_msg_full.begin_parse(); 13 | int bounced? = cs~load_uint(4) & 1; 14 | if (bounced?) { 15 | slice sender = cs~load_msg_addr(); 16 | slice ds = get_data().begin_parse().skip_bits(1); 17 | slice airdrop = ds~load_msg_addr(); 18 | throw_unless(error::wrong_sender, equal_slices(sender, airdrop)); 19 | int op = in_msg_body.skip_bits(32).preload_uint(32); 20 | throw_unless(error::wrong_operation, op == op::process_claim); 21 | set_claimed(0); 22 | } 23 | } 24 | 25 | () recv_external(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { 26 | throw_unless(error::not_enough_coins, my_balance >= const::min_balance + const::fee); 27 | slice ds = get_data().begin_parse(); 28 | throw_if(error::already_claimed, ds~load_int(1)); 29 | slice airdrop = ds~load_msg_addr(); 30 | int proof_hash = ds~load_uint(256); 31 | int index = ds~load_uint(256); 32 | 33 | int query_id = in_msg_body~load_uint(64); 34 | cell proof = in_msg_body~load_ref(); 35 | 36 | throw_unless(error::wrong_proof, proof.cell_hash() == proof_hash); 37 | 38 | accept_message(); 39 | 40 | raw_reserve(const::min_balance, 0); 41 | 42 | send_raw_message(begin_cell() 43 | .store_uint(0x18, 6) 44 | .store_slice(airdrop) 45 | .store_coins(0) 46 | .store_uint(1, 107) 47 | .store_ref(begin_cell() 48 | .store_uint(op::process_claim, 32) 49 | .store_uint(query_id, 64) 50 | .store_ref(proof) 51 | .store_uint(index, 256) 52 | .end_cell()) 53 | .end_cell(), 128); 54 | 55 | set_claimed(-1); 56 | } 57 | 58 | (int) get_claimed() method_id { 59 | return get_data().begin_parse().preload_int(1); 60 | } 61 | -------------------------------------------------------------------------------- /contracts/constants.fc: -------------------------------------------------------------------------------- 1 | const int op::deploy = 0x610ca46c; 2 | const int op::process_claim = 0x43c7d5c9; 3 | const int op::withdraw_jettons = 0x190592b2; 4 | const int op::jetton::transfer = 0x0f8a7ea5; 5 | 6 | const int error::already_claimed = 702; 7 | const int error::wrong_sender = 703; 8 | const int error::not_enough_coins = 704; 9 | const int error::wrong_proof = 705; 10 | const int error::already_deployed = 706; 11 | const int error::wrong_operation = 707; 12 | const int error::wrong_time = 708; 13 | 14 | const int const::min_balance = 50000000; 15 | const int const::fee = 50000000; 16 | -------------------------------------------------------------------------------- /contracts/imports/stdlib.fc: -------------------------------------------------------------------------------- 1 | ;; Standard library for funC 2 | ;; 3 | 4 | {- 5 | # Tuple manipulation primitives 6 | The names and the types are mostly self-explaining. 7 | See [polymorhism with forall](https://ton.org/docs/#/func/functions?id=polymorphism-with-forall) 8 | for more info on the polymorphic functions. 9 | 10 | Note that currently values of atomic type `tuple` can't be cast to composite tuple type (e.g. `[int, cell]`) 11 | and vise versa. 12 | -} 13 | 14 | {- 15 | # Lisp-style lists 16 | 17 | Lists can be represented as nested 2-elements tuples. 18 | Empty list is conventionally represented as TVM `null` value (it can be obtained by calling [null()]). 19 | For example, tuple `(1, (2, (3, null)))` represents list `[1, 2, 3]`. Elements of a list can be of different types. 20 | -} 21 | 22 | ;;; Adds an element to the beginning of lisp-style list. 23 | forall X -> tuple cons(X head, tuple tail) asm "CONS"; 24 | 25 | ;;; Extracts the head and the tail of lisp-style list. 26 | forall X -> (X, tuple) uncons(tuple list) asm "UNCONS"; 27 | 28 | ;;; Extracts the tail and the head of lisp-style list. 29 | forall X -> (tuple, X) list_next(tuple list) asm( -> 1 0) "UNCONS"; 30 | 31 | ;;; Returns the head of lisp-style list. 32 | forall X -> X car(tuple list) asm "CAR"; 33 | 34 | ;;; Returns the tail of lisp-style list. 35 | tuple cdr(tuple list) asm "CDR"; 36 | 37 | ;;; Creates tuple with zero elements. 38 | tuple empty_tuple() asm "NIL"; 39 | 40 | ;;; Appends a value `x` to a `Tuple t = (x1, ..., xn)`, but only if the resulting `Tuple t' = (x1, ..., xn, x)` 41 | ;;; is of length at most 255. Otherwise throws a type check exception. 42 | forall X -> tuple tpush(tuple t, X value) asm "TPUSH"; 43 | forall X -> (tuple, ()) ~tpush(tuple t, X value) asm "TPUSH"; 44 | 45 | ;;; Creates a tuple of length one with given argument as element. 46 | forall X -> [X] single(X x) asm "SINGLE"; 47 | 48 | ;;; Unpacks a tuple of length one 49 | forall X -> X unsingle([X] t) asm "UNSINGLE"; 50 | 51 | ;;; Creates a tuple of length two with given arguments as elements. 52 | forall X, Y -> [X, Y] pair(X x, Y y) asm "PAIR"; 53 | 54 | ;;; Unpacks a tuple of length two 55 | forall X, Y -> (X, Y) unpair([X, Y] t) asm "UNPAIR"; 56 | 57 | ;;; Creates a tuple of length three with given arguments as elements. 58 | forall X, Y, Z -> [X, Y, Z] triple(X x, Y y, Z z) asm "TRIPLE"; 59 | 60 | ;;; Unpacks a tuple of length three 61 | forall X, Y, Z -> (X, Y, Z) untriple([X, Y, Z] t) asm "UNTRIPLE"; 62 | 63 | ;;; Creates a tuple of length four with given arguments as elements. 64 | forall X, Y, Z, W -> [X, Y, Z, W] tuple4(X x, Y y, Z z, W w) asm "4 TUPLE"; 65 | 66 | ;;; Unpacks a tuple of length four 67 | forall X, Y, Z, W -> (X, Y, Z, W) untuple4([X, Y, Z, W] t) asm "4 UNTUPLE"; 68 | 69 | ;;; Returns the first element of a tuple (with unknown element types). 70 | forall X -> X first(tuple t) asm "FIRST"; 71 | 72 | ;;; Returns the second element of a tuple (with unknown element types). 73 | forall X -> X second(tuple t) asm "SECOND"; 74 | 75 | ;;; Returns the third element of a tuple (with unknown element types). 76 | forall X -> X third(tuple t) asm "THIRD"; 77 | 78 | ;;; Returns the fourth element of a tuple (with unknown element types). 79 | forall X -> X fourth(tuple t) asm "3 INDEX"; 80 | 81 | ;;; Returns the first element of a pair tuple. 82 | forall X, Y -> X pair_first([X, Y] p) asm "FIRST"; 83 | 84 | ;;; Returns the second element of a pair tuple. 85 | forall X, Y -> Y pair_second([X, Y] p) asm "SECOND"; 86 | 87 | ;;; Returns the first element of a triple tuple. 88 | forall X, Y, Z -> X triple_first([X, Y, Z] p) asm "FIRST"; 89 | 90 | ;;; Returns the second element of a triple tuple. 91 | forall X, Y, Z -> Y triple_second([X, Y, Z] p) asm "SECOND"; 92 | 93 | ;;; Returns the third element of a triple tuple. 94 | forall X, Y, Z -> Z triple_third([X, Y, Z] p) asm "THIRD"; 95 | 96 | 97 | ;;; Push null element (casted to given type) 98 | ;;; By the TVM type `Null` FunC represents absence of a value of some atomic type. 99 | ;;; So `null` can actually have any atomic type. 100 | forall X -> X null() asm "PUSHNULL"; 101 | 102 | ;;; Moves a variable [x] to the top of the stack 103 | forall X -> (X, ()) ~impure_touch(X x) impure asm "NOP"; 104 | 105 | 106 | 107 | ;;; Returns the current Unix time as an Integer 108 | int now() asm "NOW"; 109 | 110 | ;;; Returns the internal address of the current smart contract as a Slice with a `MsgAddressInt`. 111 | ;;; If necessary, it can be parsed further using primitives such as [parse_std_addr]. 112 | slice my_address() asm "MYADDR"; 113 | 114 | ;;; Returns the balance of the smart contract as a tuple consisting of an int 115 | ;;; (balance in nanotoncoins) and a `cell` 116 | ;;; (a dictionary with 32-bit keys representing the balance of "extra currencies") 117 | ;;; at the start of Computation Phase. 118 | ;;; Note that RAW primitives such as [send_raw_message] do not update this field. 119 | [int, cell] get_balance() asm "BALANCE"; 120 | 121 | ;;; Returns the logical time of the current transaction. 122 | int cur_lt() asm "LTIME"; 123 | 124 | ;;; Returns the starting logical time of the current block. 125 | int block_lt() asm "BLOCKLT"; 126 | 127 | ;;; Computes the representation hash of a `cell` [c] and returns it as a 256-bit unsigned integer `x`. 128 | ;;; Useful for signing and checking signatures of arbitrary entities represented by a tree of cells. 129 | int cell_hash(cell c) asm "HASHCU"; 130 | 131 | ;;; Computes the hash of a `slice s` and returns it as a 256-bit unsigned integer `x`. 132 | ;;; The result is the same as if an ordinary cell containing only data and references from `s` had been created 133 | ;;; and its hash computed by [cell_hash]. 134 | int slice_hash(slice s) asm "HASHSU"; 135 | 136 | ;;; Computes sha256 of the data bits of `slice` [s]. If the bit length of `s` is not divisible by eight, 137 | ;;; throws a cell underflow exception. The hash value is returned as a 256-bit unsigned integer `x`. 138 | int string_hash(slice s) asm "SHA256U"; 139 | 140 | {- 141 | # Signature checks 142 | -} 143 | 144 | ;;; Checks the Ed25519-`signature` of a `hash` (a 256-bit unsigned integer, usually computed as the hash of some data) 145 | ;;; using [public_key] (also represented by a 256-bit unsigned integer). 146 | ;;; The signature must contain at least 512 data bits; only the first 512 bits are used. 147 | ;;; The result is `−1` if the signature is valid, `0` otherwise. 148 | ;;; Note that `CHKSIGNU` creates a 256-bit slice with the hash and calls `CHKSIGNS`. 149 | ;;; That is, if [hash] is computed as the hash of some data, these data are hashed twice, 150 | ;;; the second hashing occurring inside `CHKSIGNS`. 151 | int check_signature(int hash, slice signature, int public_key) asm "CHKSIGNU"; 152 | 153 | ;;; Checks whether [signature] is a valid Ed25519-signature of the data portion of `slice data` using `public_key`, 154 | ;;; similarly to [check_signature]. 155 | ;;; If the bit length of [data] is not divisible by eight, throws a cell underflow exception. 156 | ;;; The verification of Ed25519 signatures is the standard one, 157 | ;;; with sha256 used to reduce [data] to the 256-bit number that is actually signed. 158 | int check_data_signature(slice data, slice signature, int public_key) asm "CHKSIGNS"; 159 | 160 | {--- 161 | # Computation of boc size 162 | The primitives below may be useful for computing storage fees of user-provided data. 163 | -} 164 | 165 | ;;; Returns `(x, y, z, -1)` or `(null, null, null, 0)`. 166 | ;;; Recursively computes the count of distinct cells `x`, data bits `y`, and cell references `z` 167 | ;;; in the DAG rooted at `cell` [c], effectively returning the total storage used by this DAG taking into account 168 | ;;; the identification of equal cells. 169 | ;;; The values of `x`, `y`, and `z` are computed by a depth-first traversal of this DAG, 170 | ;;; with a hash table of visited cell hashes used to prevent visits of already-visited cells. 171 | ;;; The total count of visited cells `x` cannot exceed non-negative [max_cells]; 172 | ;;; otherwise the computation is aborted before visiting the `(max_cells + 1)`-st cell and 173 | ;;; a zero flag is returned to indicate failure. If [c] is `null`, returns `x = y = z = 0`. 174 | (int, int, int) compute_data_size(cell c, int max_cells) impure asm "CDATASIZE"; 175 | 176 | ;;; Similar to [compute_data_size?], but accepting a `slice` [s] instead of a `cell`. 177 | ;;; The returned value of `x` does not take into account the cell that contains the `slice` [s] itself; 178 | ;;; however, the data bits and the cell references of [s] are accounted for in `y` and `z`. 179 | (int, int, int) slice_compute_data_size(slice s, int max_cells) impure asm "SDATASIZE"; 180 | 181 | ;;; A non-quiet version of [compute_data_size?] that throws a cell overflow exception (`8`) on failure. 182 | (int, int, int, int) compute_data_size?(cell c, int max_cells) asm "CDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; 183 | 184 | ;;; A non-quiet version of [slice_compute_data_size?] that throws a cell overflow exception (8) on failure. 185 | (int, int, int, int) slice_compute_data_size?(cell c, int max_cells) asm "SDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; 186 | 187 | ;;; Throws an exception with exit_code excno if cond is not 0 (commented since implemented in compilator) 188 | ;; () throw_if(int excno, int cond) impure asm "THROWARGIF"; 189 | 190 | {-- 191 | # Debug primitives 192 | Only works for local TVM execution with debug level verbosity 193 | -} 194 | ;;; Dumps the stack (at most the top 255 values) and shows the total stack depth. 195 | () dump_stack() impure asm "DUMPSTK"; 196 | 197 | {- 198 | # Persistent storage save and load 199 | -} 200 | 201 | ;;; Returns the persistent contract storage cell. It can be parsed or modified with slice and builder primitives later. 202 | cell get_data() asm "c4 PUSH"; 203 | 204 | ;;; Sets `cell` [c] as persistent contract data. You can update persistent contract storage with this primitive. 205 | () set_data(cell c) impure asm "c4 POP"; 206 | 207 | {- 208 | # Continuation primitives 209 | -} 210 | ;;; Usually `c3` has a continuation initialized by the whole code of the contract. It is used for function calls. 211 | ;;; The primitive returns the current value of `c3`. 212 | cont get_c3() impure asm "c3 PUSH"; 213 | 214 | ;;; Updates the current value of `c3`. Usually, it is used for updating smart contract code in run-time. 215 | ;;; Note that after execution of this primitive the current code 216 | ;;; (and the stack of recursive function calls) won't change, 217 | ;;; but any other function call will use a function from the new code. 218 | () set_c3(cont c) impure asm "c3 POP"; 219 | 220 | ;;; Transforms a `slice` [s] into a simple ordinary continuation `c`, with `c.code = s` and an empty stack and savelist. 221 | cont bless(slice s) impure asm "BLESS"; 222 | 223 | {--- 224 | # Gas related primitives 225 | -} 226 | 227 | ;;; Sets current gas limit `gl` to its maximal allowed value `gm`, and resets the gas credit `gc` to zero, 228 | ;;; decreasing the value of `gr` by `gc` in the process. 229 | ;;; In other words, the current smart contract agrees to buy some gas to finish the current transaction. 230 | ;;; This action is required to process external messages, which bring no value (hence no gas) with themselves. 231 | ;;; 232 | ;;; For more details check [accept_message effects](https://ton.org/docs/#/smart-contracts/accept). 233 | () accept_message() impure asm "ACCEPT"; 234 | 235 | ;;; Sets current gas limit `gl` to the minimum of limit and `gm`, and resets the gas credit `gc` to zero. 236 | ;;; If the gas consumed so far (including the present instruction) exceeds the resulting value of `gl`, 237 | ;;; an (unhandled) out of gas exception is thrown before setting new gas limits. 238 | ;;; Notice that [set_gas_limit] with an argument `limit ≥ 2^63 − 1` is equivalent to [accept_message]. 239 | () set_gas_limit(int limit) impure asm "SETGASLIMIT"; 240 | 241 | ;;; Commits the current state of registers `c4` (“persistent data”) and `c5` (“actions”) 242 | ;;; so that the current execution is considered “successful” with the saved values even if an exception 243 | ;;; in Computation Phase is thrown later. 244 | () commit() impure asm "COMMIT"; 245 | 246 | ;;; Not implemented 247 | ;;() buy_gas(int gram) impure asm "BUYGAS"; 248 | 249 | ;;; Computes the amount of gas that can be bought for `amount` nanoTONs, 250 | ;;; and sets `gl` accordingly in the same way as [set_gas_limit]. 251 | () buy_gas(int amount) impure asm "BUYGAS"; 252 | 253 | ;;; Computes the minimum of two integers [x] and [y]. 254 | int min(int x, int y) asm "MIN"; 255 | 256 | ;;; Computes the maximum of two integers [x] and [y]. 257 | int max(int x, int y) asm "MAX"; 258 | 259 | ;;; Sorts two integers. 260 | (int, int) minmax(int x, int y) asm "MINMAX"; 261 | 262 | ;;; Computes the absolute value of an integer [x]. 263 | int abs(int x) asm "ABS"; 264 | 265 | {- 266 | # Slice primitives 267 | 268 | It is said that a primitive _loads_ some data, 269 | if it returns the data and the remainder of the slice 270 | (so it can also be used as [modifying method](https://ton.org/docs/#/func/statements?id=modifying-methods)). 271 | 272 | It is said that a primitive _preloads_ some data, if it returns only the data 273 | (it can be used as [non-modifying method](https://ton.org/docs/#/func/statements?id=non-modifying-methods)). 274 | 275 | Unless otherwise stated, loading and preloading primitives read the data from a prefix of the slice. 276 | -} 277 | 278 | 279 | ;;; Converts a `cell` [c] into a `slice`. Notice that [c] must be either an ordinary cell, 280 | ;;; or an exotic cell (see [TVM.pdf](https://ton-blockchain.github.io/docs/tvm.pdf), 3.1.2) 281 | ;;; which is automatically loaded to yield an ordinary cell `c'`, converted into a `slice` afterwards. 282 | slice begin_parse(cell c) asm "CTOS"; 283 | 284 | ;;; Checks if [s] is empty. If not, throws an exception. 285 | () end_parse(slice s) impure asm "ENDS"; 286 | 287 | ;;; Loads the first reference from the slice. 288 | (slice, cell) load_ref(slice s) asm( -> 1 0) "LDREF"; 289 | 290 | ;;; Preloads the first reference from the slice. 291 | cell preload_ref(slice s) asm "PLDREF"; 292 | 293 | {- Functions below are commented because are implemented on compilator level for optimisation -} 294 | 295 | ;;; Loads a signed [len]-bit integer from a slice [s]. 296 | ;; (slice, int) ~load_int(slice s, int len) asm(s len -> 1 0) "LDIX"; 297 | 298 | ;;; Loads an unsigned [len]-bit integer from a slice [s]. 299 | ;; (slice, int) ~load_uint(slice s, int len) asm( -> 1 0) "LDUX"; 300 | 301 | ;;; Preloads a signed [len]-bit integer from a slice [s]. 302 | ;; int preload_int(slice s, int len) asm "PLDIX"; 303 | 304 | ;;; Preloads an unsigned [len]-bit integer from a slice [s]. 305 | ;; int preload_uint(slice s, int len) asm "PLDUX"; 306 | 307 | ;;; Loads the first `0 ≤ len ≤ 1023` bits from slice [s] into a separate `slice s''`. 308 | ;; (slice, slice) load_bits(slice s, int len) asm(s len -> 1 0) "LDSLICEX"; 309 | 310 | ;;; Preloads the first `0 ≤ len ≤ 1023` bits from slice [s] into a separate `slice s''`. 311 | ;; slice preload_bits(slice s, int len) asm "PLDSLICEX"; 312 | 313 | ;;; Loads serialized amount of TonCoins (any unsigned integer up to `2^128 - 1`). 314 | (slice, int) load_grams(slice s) asm( -> 1 0) "LDGRAMS"; 315 | (slice, int) load_coins(slice s) asm( -> 1 0) "LDGRAMS"; 316 | 317 | ;;; Returns all but the first `0 ≤ len ≤ 1023` bits of `slice` [s]. 318 | slice skip_bits(slice s, int len) asm "SDSKIPFIRST"; 319 | (slice, ()) ~skip_bits(slice s, int len) asm "SDSKIPFIRST"; 320 | 321 | ;;; Returns the first `0 ≤ len ≤ 1023` bits of `slice` [s]. 322 | slice first_bits(slice s, int len) asm "SDCUTFIRST"; 323 | 324 | ;;; Returns all but the last `0 ≤ len ≤ 1023` bits of `slice` [s]. 325 | slice skip_last_bits(slice s, int len) asm "SDSKIPLAST"; 326 | (slice, ()) ~skip_last_bits(slice s, int len) asm "SDSKIPLAST"; 327 | 328 | ;;; Returns the last `0 ≤ len ≤ 1023` bits of `slice` [s]. 329 | slice slice_last(slice s, int len) asm "SDCUTLAST"; 330 | 331 | ;;; Loads a dictionary `D` (HashMapE) from `slice` [s]. 332 | ;;; (returns `null` if `nothing` constructor is used). 333 | (slice, cell) load_dict(slice s) asm( -> 1 0) "LDDICT"; 334 | 335 | ;;; Preloads a dictionary `D` from `slice` [s]. 336 | cell preload_dict(slice s) asm "PLDDICT"; 337 | 338 | ;;; Loads a dictionary as [load_dict], but returns only the remainder of the slice. 339 | slice skip_dict(slice s) asm "SKIPDICT"; 340 | 341 | ;;; Loads (Maybe ^Cell) from `slice` [s]. 342 | ;;; In other words loads 1 bit and if it is true 343 | ;;; loads first ref and return it with slice remainder 344 | ;;; otherwise returns `null` and slice remainder 345 | (slice, cell) load_maybe_ref(slice s) asm( -> 1 0) "LDOPTREF"; 346 | 347 | ;;; Preloads (Maybe ^Cell) from `slice` [s]. 348 | cell preload_maybe_ref(slice s) asm "PLDOPTREF"; 349 | 350 | 351 | ;;; Returns the depth of `cell` [c]. 352 | ;;; If [c] has no references, then return `0`; 353 | ;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [c]. 354 | ;;; If [c] is a `null` instead of a cell, returns zero. 355 | int cell_depth(cell c) asm "CDEPTH"; 356 | 357 | 358 | {- 359 | # Slice size primitives 360 | -} 361 | 362 | ;;; Returns the number of references in `slice` [s]. 363 | int slice_refs(slice s) asm "SREFS"; 364 | 365 | ;;; Returns the number of data bits in `slice` [s]. 366 | int slice_bits(slice s) asm "SBITS"; 367 | 368 | ;;; Returns both the number of data bits and the number of references in `slice` [s]. 369 | (int, int) slice_bits_refs(slice s) asm "SBITREFS"; 370 | 371 | ;;; Checks whether a `slice` [s] is empty (i.e., contains no bits of data and no cell references). 372 | int slice_empty?(slice s) asm "SEMPTY"; 373 | 374 | ;;; Checks whether `slice` [s] has no bits of data. 375 | int slice_data_empty?(slice s) asm "SDEMPTY"; 376 | 377 | ;;; Checks whether `slice` [s] has no references. 378 | int slice_refs_empty?(slice s) asm "SREMPTY"; 379 | 380 | ;;; Returns the depth of `slice` [s]. 381 | ;;; If [s] has no references, then returns `0`; 382 | ;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [s]. 383 | int slice_depth(slice s) asm "SDEPTH"; 384 | 385 | {- 386 | # Builder size primitives 387 | -} 388 | 389 | ;;; Returns the number of cell references already stored in `builder` [b] 390 | int builder_refs(builder b) asm "BREFS"; 391 | 392 | ;;; Returns the number of data bits already stored in `builder` [b]. 393 | int builder_bits(builder b) asm "BBITS"; 394 | 395 | ;;; Returns the depth of `builder` [b]. 396 | ;;; If no cell references are stored in [b], then returns 0; 397 | ;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [b]. 398 | int builder_depth(builder b) asm "BDEPTH"; 399 | 400 | {- 401 | # Builder primitives 402 | It is said that a primitive _stores_ a value `x` into a builder `b` 403 | if it returns a modified version of the builder `b'` with the value `x` stored at the end of it. 404 | It can be used as [non-modifying method](https://ton.org/docs/#/func/statements?id=non-modifying-methods). 405 | 406 | All the primitives below first check whether there is enough space in the `builder`, 407 | and only then check the range of the value being serialized. 408 | -} 409 | 410 | ;;; Creates a new empty `builder`. 411 | builder begin_cell() asm "NEWC"; 412 | 413 | ;;; Converts a `builder` into an ordinary `cell`. 414 | cell end_cell(builder b) asm "ENDC"; 415 | 416 | ;;; Stores a reference to `cell` [c] into `builder` [b]. 417 | builder store_ref(builder b, cell c) asm(c b) "STREF"; 418 | 419 | ;;; Stores an unsigned [len]-bit integer `x` into `b` for `0 ≤ len ≤ 256`. 420 | ;; builder store_uint(builder b, int x, int len) asm(x b len) "STUX"; 421 | 422 | ;;; Stores a signed [len]-bit integer `x` into `b` for` 0 ≤ len ≤ 257`. 423 | ;; builder store_int(builder b, int x, int len) asm(x b len) "STIX"; 424 | 425 | 426 | ;;; Stores `slice` [s] into `builder` [b] 427 | builder store_slice(builder b, slice s) asm "STSLICER"; 428 | 429 | ;;; Stores (serializes) an integer [x] in the range `0..2^128 − 1` into `builder` [b]. 430 | ;;; The serialization of [x] consists of a 4-bit unsigned big-endian integer `l`, 431 | ;;; which is the smallest integer `l ≥ 0`, such that `x < 2^8l`, 432 | ;;; followed by an `8l`-bit unsigned big-endian representation of [x]. 433 | ;;; If [x] does not belong to the supported range, a range check exception is thrown. 434 | ;;; 435 | ;;; Store amounts of TonCoins to the builder as VarUInteger 16 436 | builder store_grams(builder b, int x) asm "STGRAMS"; 437 | builder store_coins(builder b, int x) asm "STGRAMS"; 438 | 439 | ;;; Stores dictionary `D` represented by `cell` [c] or `null` into `builder` [b]. 440 | ;;; In other words, stores a `1`-bit and a reference to [c] if [c] is not `null` and `0`-bit otherwise. 441 | builder store_dict(builder b, cell c) asm(c b) "STDICT"; 442 | 443 | ;;; Stores (Maybe ^Cell) to builder: 444 | ;;; if cell is null store 1 zero bit 445 | ;;; otherwise store 1 true bit and ref to cell 446 | builder store_maybe_ref(builder b, cell c) asm(c b) "STOPTREF"; 447 | 448 | 449 | {- 450 | # Address manipulation primitives 451 | The address manipulation primitives listed below serialize and deserialize values according to the following TL-B scheme: 452 | ```TL-B 453 | addr_none$00 = MsgAddressExt; 454 | addr_extern$01 len:(## 8) external_address:(bits len) 455 | = MsgAddressExt; 456 | anycast_info$_ depth:(#<= 30) { depth >= 1 } 457 | rewrite_pfx:(bits depth) = Anycast; 458 | addr_std$10 anycast:(Maybe Anycast) 459 | workchain_id:int8 address:bits256 = MsgAddressInt; 460 | addr_var$11 anycast:(Maybe Anycast) addr_len:(## 9) 461 | workchain_id:int32 address:(bits addr_len) = MsgAddressInt; 462 | _ _:MsgAddressInt = MsgAddress; 463 | _ _:MsgAddressExt = MsgAddress; 464 | 465 | int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool 466 | src:MsgAddress dest:MsgAddressInt 467 | value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams 468 | created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed; 469 | ext_out_msg_info$11 src:MsgAddress dest:MsgAddressExt 470 | created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed; 471 | ``` 472 | A deserialized `MsgAddress` is represented by a tuple `t` as follows: 473 | 474 | - `addr_none` is represented by `t = (0)`, 475 | i.e., a tuple containing exactly one integer equal to zero. 476 | - `addr_extern` is represented by `t = (1, s)`, 477 | where slice `s` contains the field `external_address`. In other words, ` 478 | t` is a pair (a tuple consisting of two entries), containing an integer equal to one and slice `s`. 479 | - `addr_std` is represented by `t = (2, u, x, s)`, 480 | where `u` is either a `null` (if `anycast` is absent) or a slice `s'` containing `rewrite_pfx` (if anycast is present). 481 | Next, integer `x` is the `workchain_id`, and slice `s` contains the address. 482 | - `addr_var` is represented by `t = (3, u, x, s)`, 483 | where `u`, `x`, and `s` have the same meaning as for `addr_std`. 484 | -} 485 | 486 | ;;; Loads from slice [s] the only prefix that is a valid `MsgAddress`, 487 | ;;; and returns both this prefix `s'` and the remainder `s''` of [s] as slices. 488 | (slice, slice) load_msg_addr(slice s) asm( -> 1 0) "LDMSGADDR"; 489 | 490 | ;;; Decomposes slice [s] containing a valid `MsgAddress` into a `tuple t` with separate fields of this `MsgAddress`. 491 | ;;; If [s] is not a valid `MsgAddress`, a cell deserialization exception is thrown. 492 | tuple parse_addr(slice s) asm "PARSEMSGADDR"; 493 | 494 | ;;; Parses slice [s] containing a valid `MsgAddressInt` (usually a `msg_addr_std`), 495 | ;;; applies rewriting from the anycast (if present) to the same-length prefix of the address, 496 | ;;; and returns both the workchain and the 256-bit address as integers. 497 | ;;; If the address is not 256-bit, or if [s] is not a valid serialization of `MsgAddressInt`, 498 | ;;; throws a cell deserialization exception. 499 | (int, int) parse_std_addr(slice s) asm "REWRITESTDADDR"; 500 | 501 | ;;; A variant of [parse_std_addr] that returns the (rewritten) address as a slice [s], 502 | ;;; even if it is not exactly 256 bit long (represented by a `msg_addr_var`). 503 | (int, slice) parse_var_addr(slice s) asm "REWRITEVARADDR"; 504 | 505 | {- 506 | # Dictionary primitives 507 | -} 508 | 509 | 510 | ;;; Sets the value associated with [key_len]-bit key signed index in dictionary [dict] to [value] (cell), 511 | ;;; and returns the resulting dictionary. 512 | cell idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; 513 | (cell, ()) ~idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; 514 | 515 | ;;; Sets the value associated with [key_len]-bit key unsigned index in dictionary [dict] to [value] (cell), 516 | ;;; and returns the resulting dictionary. 517 | cell udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; 518 | (cell, ()) ~udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; 519 | 520 | cell idict_get_ref(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETOPTREF"; 521 | (cell, int) idict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETREF" "NULLSWAPIFNOT"; 522 | (cell, int) udict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETREF" "NULLSWAPIFNOT"; 523 | (cell, cell) idict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETGETOPTREF"; 524 | (cell, cell) udict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETGETOPTREF"; 525 | (cell, int) idict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDEL"; 526 | (cell, int) udict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDEL"; 527 | (slice, int) idict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGET" "NULLSWAPIFNOT"; 528 | (slice, int) udict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGET" "NULLSWAPIFNOT"; 529 | (cell, slice, int) idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; 530 | (cell, slice, int) udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; 531 | (cell, (slice, int)) ~idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; 532 | (cell, (slice, int)) ~udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; 533 | cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; 534 | (cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; 535 | cell idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; 536 | (cell, ()) ~idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; 537 | cell dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; 538 | (cell, ()) ~dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; 539 | (cell, int) udict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUADD"; 540 | (cell, int) udict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUREPLACE"; 541 | (cell, int) idict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIADD"; 542 | (cell, int) idict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIREPLACE"; 543 | cell udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; 544 | (cell, ()) ~udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; 545 | cell idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; 546 | (cell, ()) ~idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; 547 | cell dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; 548 | (cell, ()) ~dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; 549 | (cell, int) udict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUADDB"; 550 | (cell, int) udict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUREPLACEB"; 551 | (cell, int) idict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIADDB"; 552 | (cell, int) idict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIREPLACEB"; 553 | (cell, int, slice, int) udict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; 554 | (cell, (int, slice, int)) ~udict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; 555 | (cell, int, slice, int) idict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; 556 | (cell, (int, slice, int)) ~idict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; 557 | (cell, slice, slice, int) dict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; 558 | (cell, (slice, slice, int)) ~dict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; 559 | (cell, int, slice, int) udict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; 560 | (cell, (int, slice, int)) ~udict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; 561 | (cell, int, slice, int) idict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; 562 | (cell, (int, slice, int)) ~idict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; 563 | (cell, slice, slice, int) dict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; 564 | (cell, (slice, slice, int)) ~dict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; 565 | (int, slice, int) udict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMIN" "NULLSWAPIFNOT2"; 566 | (int, slice, int) udict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAX" "NULLSWAPIFNOT2"; 567 | (int, cell, int) udict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMINREF" "NULLSWAPIFNOT2"; 568 | (int, cell, int) udict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAXREF" "NULLSWAPIFNOT2"; 569 | (int, slice, int) idict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMIN" "NULLSWAPIFNOT2"; 570 | (int, slice, int) idict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAX" "NULLSWAPIFNOT2"; 571 | (int, cell, int) idict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMINREF" "NULLSWAPIFNOT2"; 572 | (int, cell, int) idict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAXREF" "NULLSWAPIFNOT2"; 573 | (int, slice, int) udict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXT" "NULLSWAPIFNOT2"; 574 | (int, slice, int) udict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXTEQ" "NULLSWAPIFNOT2"; 575 | (int, slice, int) udict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREV" "NULLSWAPIFNOT2"; 576 | (int, slice, int) udict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREVEQ" "NULLSWAPIFNOT2"; 577 | (int, slice, int) idict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXT" "NULLSWAPIFNOT2"; 578 | (int, slice, int) idict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXTEQ" "NULLSWAPIFNOT2"; 579 | (int, slice, int) idict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREV" "NULLSWAPIFNOT2"; 580 | (int, slice, int) idict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREVEQ" "NULLSWAPIFNOT2"; 581 | 582 | ;;; Creates an empty dictionary, which is actually a null value. Equivalent to PUSHNULL 583 | cell new_dict() asm "NEWDICT"; 584 | ;;; Checks whether a dictionary is empty. Equivalent to cell_null?. 585 | int dict_empty?(cell c) asm "DICTEMPTY"; 586 | 587 | 588 | {- Prefix dictionary primitives -} 589 | (slice, slice, slice, int) pfxdict_get?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTGETQ" "NULLSWAPIFNOT2"; 590 | (cell, int) pfxdict_set?(cell dict, int key_len, slice key, slice value) asm(value key dict key_len) "PFXDICTSET"; 591 | (cell, int) pfxdict_delete?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTDEL"; 592 | 593 | ;;; Returns the value of the global configuration parameter with integer index `i` as a `cell` or `null` value. 594 | cell config_param(int x) asm "CONFIGOPTPARAM"; 595 | ;;; Checks whether c is a null. Note, that FunC also has polymorphic null? built-in. 596 | int cell_null?(cell c) asm "ISNULL"; 597 | 598 | ;;; Creates an output action which would reserve exactly amount nanotoncoins (if mode = 0), at most amount nanotoncoins (if mode = 2), or all but amount nanotoncoins (if mode = 1 or mode = 3), from the remaining balance of the account. It is roughly equivalent to creating an outbound message carrying amount nanotoncoins (or b − amount nanotoncoins, where b is the remaining balance) to oneself, so that the subsequent output actions would not be able to spend more money than the remainder. Bit +2 in mode means that the external action does not fail if the specified amount cannot be reserved; instead, all remaining balance is reserved. Bit +8 in mode means `amount <- -amount` before performing any further actions. Bit +4 in mode means that amount is increased by the original balance of the current account (before the compute phase), including all extra currencies, before performing any other checks and actions. Currently, amount must be a non-negative integer, and mode must be in the range 0..15. 599 | () raw_reserve(int amount, int mode) impure asm "RAWRESERVE"; 600 | ;;; Similar to raw_reserve, but also accepts a dictionary extra_amount (represented by a cell or null) with extra currencies. In this way currencies other than TonCoin can be reserved. 601 | () raw_reserve_extra(int amount, cell extra_amount, int mode) impure asm "RAWRESERVEX"; 602 | ;;; Sends a raw message contained in msg, which should contain a correctly serialized object Message X, with the only exception that the source address is allowed to have dummy value addr_none (to be automatically replaced with the current smart contract address), and ihr_fee, fwd_fee, created_lt and created_at fields can have arbitrary values (to be rewritten with correct values during the action phase of the current transaction). Integer parameter mode contains the flags. Currently mode = 0 is used for ordinary messages; mode = 128 is used for messages that are to carry all the remaining balance of the current smart contract (instead of the value originally indicated in the message); mode = 64 is used for messages that carry all the remaining value of the inbound message in addition to the value initially indicated in the new message (if bit 0 is not set, the gas fees are deducted from this amount); mode' = mode + 1 means that the sender wants to pay transfer fees separately; mode' = mode + 2 means that any errors arising while processing this message during the action phase should be ignored. Finally, mode' = mode + 32 means that the current account must be destroyed if its resulting balance is zero. This flag is usually employed together with +128. 603 | () send_raw_message(cell msg, int mode) impure asm "SENDRAWMSG"; 604 | ;;; Creates an output action that would change this smart contract code to that given by cell new_code. Notice that this change will take effect only after the successful termination of the current run of the smart contract 605 | () set_code(cell new_code) impure asm "SETCODE"; 606 | 607 | ;;; Generates a new pseudo-random unsigned 256-bit integer x. The algorithm is as follows: if r is the old value of the random seed, considered as a 32-byte array (by constructing the big-endian representation of an unsigned 256-bit integer), then its sha512(r) is computed; the first 32 bytes of this hash are stored as the new value r' of the random seed, and the remaining 32 bytes are returned as the next random value x. 608 | int random() impure asm "RANDU256"; 609 | ;;; Generates a new pseudo-random integer z in the range 0..range−1 (or range..−1, if range < 0). More precisely, an unsigned random value x is generated as in random; then z := x * range / 2^256 is computed. 610 | int rand(int range) impure asm "RAND"; 611 | ;;; Returns the current random seed as an unsigned 256-bit Integer. 612 | int get_seed() impure asm "RANDSEED"; 613 | ;;; Sets the random seed to unsigned 256-bit seed. 614 | () set_seed(int x) impure asm "SETRAND"; 615 | ;;; Mixes unsigned 256-bit integer x into the random seed r by setting the random seed to sha256 of the concatenation of two 32-byte strings: the first with the big-endian representation of the old seed r, and the second with the big-endian representation of x. 616 | () randomize(int x) impure asm "ADDRAND"; 617 | ;;; Equivalent to randomize(cur_lt());. 618 | () randomize_lt() impure asm "LTIME" "ADDRAND"; 619 | 620 | ;;; Checks whether the data parts of two slices coinside 621 | int equal_slice_bits(slice a, slice b) asm "SDEQ"; 622 | int equal_slices(slice a, slice b) asm "SDEQ"; 623 | 624 | ;;; Concatenates two builders 625 | builder store_builder(builder to, builder from) asm "STBR"; -------------------------------------------------------------------------------- /contracts/jetton/jetton-utils.fc: -------------------------------------------------------------------------------- 1 | #include "../imports/stdlib.fc"; 2 | #include "params.fc"; 3 | 4 | cell pack_jetton_wallet_data(int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline { 5 | return begin_cell() 6 | .store_coins(balance) 7 | .store_slice(owner_address) 8 | .store_slice(jetton_master_address) 9 | .store_ref(jetton_wallet_code) 10 | .end_cell(); 11 | } 12 | 13 | cell calculate_jetton_wallet_state_init(slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline { 14 | return begin_cell() 15 | .store_uint(0, 2) 16 | .store_dict(jetton_wallet_code) 17 | .store_dict(pack_jetton_wallet_data(0, owner_address, jetton_master_address, jetton_wallet_code)) 18 | .store_uint(0, 1) 19 | .end_cell(); 20 | } 21 | 22 | slice calculate_jetton_wallet_address(cell state_init) inline { 23 | return begin_cell().store_uint(4, 3) 24 | .store_int(workchain(), 8) 25 | .store_uint(cell_hash(state_init), 256) 26 | .end_cell() 27 | .begin_parse(); 28 | } 29 | 30 | slice calculate_user_jetton_wallet_address(slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline { 31 | return calculate_jetton_wallet_address(calculate_jetton_wallet_state_init(owner_address, jetton_master_address, jetton_wallet_code)); 32 | } 33 | -------------------------------------------------------------------------------- /contracts/jetton/jetton_minter.fc: -------------------------------------------------------------------------------- 1 | #include "../imports/stdlib.fc"; 2 | #include "params.fc"; 3 | #include "op-codes.fc"; 4 | #include "jetton-utils.fc"; 5 | 6 | ;; It is recommended to use https://github.com/ton-blockchain/token-contract/blob/main/ft/jetton-minter-discoverable.fc 7 | ;; instead of this contract, see https://github.com/ton-blockchain/TEPs/blob/master/text/0089-jetton-wallet-discovery.md 8 | 9 | ;; Jettons minter smart contract 10 | 11 | ;; storage scheme 12 | ;; storage#_ total_supply:Coins admin_address:MsgAddress content:^Cell jetton_wallet_code:^Cell = Storage; 13 | 14 | (int, slice, cell, cell) load_data() inline { 15 | slice ds = get_data().begin_parse(); 16 | return ( 17 | ds~load_coins(), ;; total_supply 18 | ds~load_msg_addr(), ;; admin_address 19 | ds~load_ref(), ;; content 20 | ds~load_ref() ;; jetton_wallet_code 21 | ); 22 | } 23 | 24 | () save_data(int total_supply, slice admin_address, cell content, cell jetton_wallet_code) impure inline { 25 | set_data(begin_cell() 26 | .store_coins(total_supply) 27 | .store_slice(admin_address) 28 | .store_ref(content) 29 | .store_ref(jetton_wallet_code) 30 | .end_cell() 31 | ); 32 | } 33 | 34 | () mint_tokens(slice to_address, cell jetton_wallet_code, int amount, cell master_msg) impure { 35 | cell state_init = calculate_jetton_wallet_state_init(to_address, my_address(), jetton_wallet_code); 36 | slice to_wallet_address = calculate_jetton_wallet_address(state_init); 37 | var msg = begin_cell() 38 | .store_uint(0x18, 6) 39 | .store_slice(to_wallet_address) 40 | .store_coins(amount) 41 | .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) 42 | .store_ref(state_init) 43 | .store_ref(master_msg); 44 | send_raw_message(msg.end_cell(), 1); ;; pay transfer fees separately, revert on errors 45 | } 46 | 47 | () recv_internal(int msg_value, cell in_msg_full, slice in_msg_body) impure { 48 | if (in_msg_body.slice_empty?()) { ;; ignore empty messages 49 | return (); 50 | } 51 | slice cs = in_msg_full.begin_parse(); 52 | int flags = cs~load_uint(4); 53 | 54 | if (flags & 1) { ;; ignore all bounced messages 55 | return (); 56 | } 57 | slice sender_address = cs~load_msg_addr(); 58 | 59 | int op = in_msg_body~load_uint(32); 60 | int query_id = in_msg_body~load_uint(64); 61 | 62 | (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data(); 63 | 64 | if (op == op::mint()) { 65 | throw_unless(73, equal_slices(sender_address, admin_address)); 66 | slice to_address = in_msg_body~load_msg_addr(); 67 | int amount = in_msg_body~load_coins(); 68 | cell master_msg = in_msg_body~load_ref(); 69 | slice master_msg_cs = master_msg.begin_parse(); 70 | master_msg_cs~skip_bits(32 + 64); ;; op + query_id 71 | int jetton_amount = master_msg_cs~load_coins(); 72 | mint_tokens(to_address, jetton_wallet_code, amount, master_msg); 73 | save_data(total_supply + jetton_amount, admin_address, content, jetton_wallet_code); 74 | return (); 75 | } 76 | 77 | if (op == op::burn_notification()) { 78 | int jetton_amount = in_msg_body~load_coins(); 79 | slice from_address = in_msg_body~load_msg_addr(); 80 | throw_unless(74, 81 | equal_slices(calculate_user_jetton_wallet_address(from_address, my_address(), jetton_wallet_code), sender_address) 82 | ); 83 | save_data(total_supply - jetton_amount, admin_address, content, jetton_wallet_code); 84 | slice response_address = in_msg_body~load_msg_addr(); 85 | if (response_address.preload_uint(2) != 0) { 86 | var msg = begin_cell() 87 | .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000 88 | .store_slice(response_address) 89 | .store_coins(0) 90 | .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) 91 | .store_uint(op::excesses(), 32) 92 | .store_uint(query_id, 64); 93 | send_raw_message(msg.end_cell(), 2 + 64); 94 | } 95 | return (); 96 | } 97 | 98 | if (op == 3) { ;; change admin 99 | throw_unless(73, equal_slices(sender_address, admin_address)); 100 | slice new_admin_address = in_msg_body~load_msg_addr(); 101 | save_data(total_supply, new_admin_address, content, jetton_wallet_code); 102 | return (); 103 | } 104 | 105 | if (op == 4) { ;; change content, delete this for immutable tokens 106 | throw_unless(73, equal_slices(sender_address, admin_address)); 107 | save_data(total_supply, admin_address, in_msg_body~load_ref(), jetton_wallet_code); 108 | return (); 109 | } 110 | 111 | throw(0xffff); 112 | } 113 | 114 | (int, int, slice, cell, cell) get_jetton_data() method_id { 115 | (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data(); 116 | return (total_supply, -1, admin_address, content, jetton_wallet_code); 117 | } 118 | 119 | slice get_wallet_address(slice owner_address) method_id { 120 | (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data(); 121 | return calculate_user_jetton_wallet_address(owner_address, my_address(), jetton_wallet_code); 122 | } -------------------------------------------------------------------------------- /contracts/jetton/jetton_wallet.fc: -------------------------------------------------------------------------------- 1 | #include "../imports/stdlib.fc"; 2 | #include "params.fc"; 3 | #include "op-codes.fc"; 4 | #include "jetton-utils.fc"; 5 | 6 | ;; Jetton Wallet Smart Contract 7 | 8 | {- 9 | 10 | NOTE that this tokens can be transferred within the same workchain. 11 | 12 | This is suitable for most tokens, if you need tokens transferable between workchains there are two solutions: 13 | 14 | 1) use more expensive but universal function to calculate message forward fee for arbitrary destination (see `misc/forward-fee-calc.cs`) 15 | 16 | 2) use token holder proxies in target workchain (that way even 'non-universal' token can be used from any workchain) 17 | 18 | -} 19 | 20 | int min_tons_for_storage() asm "10000000 PUSHINT"; ;; 0.01 TON 21 | ;; Note that 2 * gas_consumptions is expected to be able to cover fees on both wallets (sender and receiver) 22 | ;; and also constant fees on inter-wallet interaction, in particular fwd fee on state_init transfer 23 | ;; that means that you need to reconsider this fee when: 24 | ;; a) jetton logic become more gas-heavy 25 | ;; b) jetton-wallet code (sent with inter-wallet message) become larger or smaller 26 | ;; c) global fee changes / different workchain 27 | int gas_consumption() asm "15000000 PUSHINT"; ;; 0.015 TON 28 | 29 | {- 30 | Storage 31 | storage#_ balance:Coins owner_address:MsgAddressInt jetton_master_address:MsgAddressInt jetton_wallet_code:^Cell = Storage; 32 | -} 33 | 34 | (int, slice, slice, cell) load_data() inline { 35 | slice ds = get_data().begin_parse(); 36 | return (ds~load_coins(), ds~load_msg_addr(), ds~load_msg_addr(), ds~load_ref()); 37 | } 38 | 39 | () save_data (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) impure inline { 40 | set_data(pack_jetton_wallet_data(balance, owner_address, jetton_master_address, jetton_wallet_code)); 41 | } 42 | 43 | {- 44 | transfer query_id:uint64 amount:(VarUInteger 16) destination:MsgAddress 45 | response_destination:MsgAddress custom_payload:(Maybe ^Cell) 46 | forward_ton_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell) 47 | = InternalMsgBody; 48 | internal_transfer query_id:uint64 amount:(VarUInteger 16) from:MsgAddress 49 | response_address:MsgAddress 50 | forward_ton_amount:(VarUInteger 16) 51 | forward_payload:(Either Cell ^Cell) 52 | = InternalMsgBody; 53 | -} 54 | 55 | () send_tokens (slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure { 56 | int query_id = in_msg_body~load_uint(64); 57 | int jetton_amount = in_msg_body~load_coins(); 58 | slice to_owner_address = in_msg_body~load_msg_addr(); 59 | force_chain(to_owner_address); 60 | (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); 61 | balance -= jetton_amount; 62 | 63 | throw_unless(705, equal_slices(owner_address, sender_address)); 64 | throw_unless(706, balance >= 0); 65 | 66 | cell state_init = calculate_jetton_wallet_state_init(to_owner_address, jetton_master_address, jetton_wallet_code); 67 | slice to_wallet_address = calculate_jetton_wallet_address(state_init); 68 | slice response_address = in_msg_body~load_msg_addr(); 69 | cell custom_payload = in_msg_body~load_dict(); 70 | int forward_ton_amount = in_msg_body~load_coins(); 71 | throw_unless(708, slice_bits(in_msg_body) >= 1); 72 | slice either_forward_payload = in_msg_body; 73 | var msg = begin_cell() 74 | .store_uint(0x18, 6) 75 | .store_slice(to_wallet_address) 76 | .store_coins(0) 77 | .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) 78 | .store_ref(state_init); 79 | var msg_body = begin_cell() 80 | .store_uint(op::internal_transfer(), 32) 81 | .store_uint(query_id, 64) 82 | .store_coins(jetton_amount) 83 | .store_slice(owner_address) 84 | .store_slice(response_address) 85 | .store_coins(forward_ton_amount) 86 | .store_slice(either_forward_payload) 87 | .end_cell(); 88 | 89 | msg = msg.store_ref(msg_body); 90 | int fwd_count = forward_ton_amount ? 2 : 1; 91 | throw_unless(709, msg_value > 92 | forward_ton_amount + 93 | ;; 3 messages: wal1->wal2, wal2->owner, wal2->response 94 | ;; but last one is optional (it is ok if it fails) 95 | fwd_count * fwd_fee + 96 | (2 * gas_consumption() + min_tons_for_storage())); 97 | ;; universal message send fee calculation may be activated here 98 | ;; by using this instead of fwd_fee 99 | ;; msg_fwd_fee(to_wallet, msg_body, state_init, 15) 100 | 101 | send_raw_message(msg.end_cell(), 64); ;; revert on errors 102 | save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); 103 | } 104 | 105 | {- 106 | internal_transfer query_id:uint64 amount:(VarUInteger 16) from:MsgAddress 107 | response_address:MsgAddress 108 | forward_ton_amount:(VarUInteger 16) 109 | forward_payload:(Either Cell ^Cell) 110 | = InternalMsgBody; 111 | -} 112 | 113 | () receive_tokens (slice in_msg_body, slice sender_address, int my_ton_balance, int fwd_fee, int msg_value) impure { 114 | ;; NOTE we can not allow fails in action phase since in that case there will be 115 | ;; no bounce. Thus check and throw in computation phase. 116 | (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); 117 | int query_id = in_msg_body~load_uint(64); 118 | int jetton_amount = in_msg_body~load_coins(); 119 | balance += jetton_amount; 120 | slice from_address = in_msg_body~load_msg_addr(); 121 | slice response_address = in_msg_body~load_msg_addr(); 122 | throw_unless(707, 123 | equal_slices(jetton_master_address, sender_address) 124 | | 125 | equal_slices(calculate_user_jetton_wallet_address(from_address, jetton_master_address, jetton_wallet_code), sender_address) 126 | ); 127 | int forward_ton_amount = in_msg_body~load_coins(); 128 | 129 | int ton_balance_before_msg = my_ton_balance - msg_value; 130 | int storage_fee = min_tons_for_storage() - min(ton_balance_before_msg, min_tons_for_storage()); 131 | msg_value -= (storage_fee + gas_consumption()); 132 | if(forward_ton_amount) { 133 | msg_value -= (forward_ton_amount + fwd_fee); 134 | slice either_forward_payload = in_msg_body; 135 | 136 | var msg_body = begin_cell() 137 | .store_uint(op::transfer_notification(), 32) 138 | .store_uint(query_id, 64) 139 | .store_coins(jetton_amount) 140 | .store_slice(from_address) 141 | .store_slice(either_forward_payload) 142 | .end_cell(); 143 | 144 | var msg = begin_cell() 145 | .store_uint(0x10, 6) ;; we should not bounce here cause receiver can have uninitialized contract 146 | .store_slice(owner_address) 147 | .store_coins(forward_ton_amount) 148 | .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) 149 | .store_ref(msg_body); 150 | send_raw_message(msg.end_cell(), 1); 151 | } 152 | 153 | if ((response_address.preload_uint(2) != 0) & (msg_value > 0)) { 154 | var msg = begin_cell() 155 | .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 010000 156 | .store_slice(response_address) 157 | .store_coins(msg_value) 158 | .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) 159 | .store_uint(op::excesses(), 32) 160 | .store_uint(query_id, 64); 161 | send_raw_message(msg.end_cell(), 2); 162 | } 163 | 164 | save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); 165 | } 166 | 167 | () burn_tokens (slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure { 168 | ;; NOTE we can not allow fails in action phase since in that case there will be 169 | ;; no bounce. Thus check and throw in computation phase. 170 | (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); 171 | int query_id = in_msg_body~load_uint(64); 172 | int jetton_amount = in_msg_body~load_coins(); 173 | slice response_address = in_msg_body~load_msg_addr(); 174 | ;; ignore custom payload 175 | ;; slice custom_payload = in_msg_body~load_dict(); 176 | balance -= jetton_amount; 177 | throw_unless(705, equal_slices(owner_address, sender_address)); 178 | throw_unless(706, balance >= 0); 179 | throw_unless(707, msg_value > fwd_fee + 2 * gas_consumption()); 180 | 181 | var msg_body = begin_cell() 182 | .store_uint(op::burn_notification(), 32) 183 | .store_uint(query_id, 64) 184 | .store_coins(jetton_amount) 185 | .store_slice(owner_address) 186 | .store_slice(response_address) 187 | .end_cell(); 188 | 189 | var msg = begin_cell() 190 | .store_uint(0x18, 6) 191 | .store_slice(jetton_master_address) 192 | .store_coins(0) 193 | .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) 194 | .store_ref(msg_body); 195 | 196 | send_raw_message(msg.end_cell(), 64); 197 | 198 | save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); 199 | } 200 | 201 | () on_bounce (slice in_msg_body) impure { 202 | in_msg_body~skip_bits(32); ;; 0xFFFFFFFF 203 | (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); 204 | int op = in_msg_body~load_uint(32); 205 | throw_unless(709, (op == op::internal_transfer()) | (op == op::burn_notification())); 206 | int query_id = in_msg_body~load_uint(64); 207 | int jetton_amount = in_msg_body~load_coins(); 208 | balance += jetton_amount; 209 | save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); 210 | } 211 | 212 | () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { 213 | if (in_msg_body.slice_empty?()) { ;; ignore empty messages 214 | return (); 215 | } 216 | 217 | slice cs = in_msg_full.begin_parse(); 218 | int flags = cs~load_uint(4); 219 | if (flags & 1) { 220 | on_bounce(in_msg_body); 221 | return (); 222 | } 223 | slice sender_address = cs~load_msg_addr(); 224 | cs~load_msg_addr(); ;; skip dst 225 | cs~load_coins(); ;; skip value 226 | cs~skip_bits(1); ;; skip extracurrency collection 227 | cs~load_coins(); ;; skip ihr_fee 228 | int fwd_fee = muldiv(cs~load_coins(), 3, 2); ;; we use message fwd_fee for estimation of forward_payload costs 229 | 230 | int op = in_msg_body~load_uint(32); 231 | 232 | if (op == op::transfer()) { ;; outgoing transfer 233 | send_tokens(in_msg_body, sender_address, msg_value, fwd_fee); 234 | return (); 235 | } 236 | 237 | if (op == op::internal_transfer()) { ;; incoming transfer 238 | receive_tokens(in_msg_body, sender_address, my_balance, fwd_fee, msg_value); 239 | return (); 240 | } 241 | 242 | if (op == op::burn()) { ;; burn 243 | burn_tokens(in_msg_body, sender_address, msg_value, fwd_fee); 244 | return (); 245 | } 246 | 247 | throw(0xffff); 248 | } 249 | 250 | (int, slice, slice, cell) get_wallet_data() method_id { 251 | return load_data(); 252 | } -------------------------------------------------------------------------------- /contracts/jetton/op-codes.fc: -------------------------------------------------------------------------------- 1 | int op::transfer() asm "0xf8a7ea5 PUSHINT"; 2 | int op::transfer_notification() asm "0x7362d09c PUSHINT"; 3 | int op::internal_transfer() asm "0x178d4519 PUSHINT"; 4 | int op::excesses() asm "0xd53276db PUSHINT"; 5 | int op::burn() asm "0x595f07bc PUSHINT"; 6 | int op::burn_notification() asm "0x7bdd97de PUSHINT"; 7 | 8 | ;; Minter 9 | int op::mint() asm "21 PUSHINT"; -------------------------------------------------------------------------------- /contracts/jetton/params.fc: -------------------------------------------------------------------------------- 1 | #include "../imports/stdlib.fc"; 2 | 3 | int workchain() asm "0 PUSHINT"; 4 | 5 | () force_chain(slice addr) impure { 6 | (int wc, _) = parse_std_addr(addr); 7 | throw_unless(333, wc == workchain()); 8 | } -------------------------------------------------------------------------------- /contracts/scheme.tlb: -------------------------------------------------------------------------------- 1 | _ jetton_wallet:MsgAddressInt merkle_root:uint256 helper_code:^Cell begin:uint64 admin:MsgAddressInt seed:uint64 = AirdropStorage; 2 | 3 | _ claimed:Bool airdrop:MsgAddressInt proof_hash:uint256 index:uint256 = AirdropHelperStorage; 4 | 5 | _ address:MsgAddressInt amount:Coins = AirdropEntry; 6 | 7 | deploy#610ca46c jetton_wallet:MsgAddressInt = InternalMsgBody; 8 | 9 | claim#_ query_id:uint64 proof:^(MerkleProof (HashmapE 256 AirdropEntry)) = ExternalMsgBody; 10 | 11 | process_claim#43c7d5c9 query_id:uint64 proof:^(MerkleProof (HashmapE 256 AirdropEntry)) index:uint256 = InternalMsgBody; 12 | 13 | withdraw_jettons#190592b2 query_id:uint64 amount:Coins = InternalMsgBody; 14 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | testPathIgnorePatterns: ['/node_modules/', '/dist/'], 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airdrop", 3 | "license": "MIT", 4 | "scripts": { 5 | "test": "jest", 6 | "build": "blueprint build --all && tsc --build" 7 | }, 8 | "devDependencies": { 9 | "@ton/blueprint": "^0.12.1", 10 | "@ton/core": "^0.53.0", 11 | "@ton/crypto": "^3.2.0", 12 | "@ton/sandbox": "^0.11.1", 13 | "@ton/test-utils": "^0.3.1", 14 | "@ton/ton": "^13.7.0", 15 | "@types/jest": "^29.5.0", 16 | "@types/node": "^18.15.5", 17 | "jest": "^29.5.0", 18 | "prettier": "^2.8.6", 19 | "ts-jest": "^29.0.5", 20 | "ts-node": "^10.9.1", 21 | "typescript": "^4.9.5" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /scripts/claimAirdrop.ts: -------------------------------------------------------------------------------- 1 | import { Address, Cell, Dictionary } from '@ton/core'; 2 | import { airdropEntryValue } from '../wrappers/Airdrop'; 3 | import { NetworkProvider, compile } from '@ton/blueprint'; 4 | import { AirdropHelper } from '../wrappers/AirdropHelper'; 5 | 6 | export async function run(provider: NetworkProvider) { 7 | // suppose that you have the cell in base64 form stored somewhere 8 | const dictCell = Cell.fromBase64( 9 | 'te6cckEBBQEAhgACA8/oAgEATUgA8OYDSxw0XZi4OdCD0hNOBW2Fd/rkR/Wmvmc3OwLdEYiLLQXgEAIBIAQDAE0gAkQn3LTRp9vn/K0TXJrWPCeEmrX7VdoMP2KoakM4TmSaO5rKAEAATSACVAuEaWe9itDZsX37JEAijrTCMPqXgvii2bYEKL67Q5odzWUAQC6Eo5U=' 10 | ); 11 | const dict = dictCell.beginParse().loadDictDirect(Dictionary.Keys.BigUint(256), airdropEntryValue); 12 | 13 | const entryIndex = 2n; 14 | 15 | const proof = dict.generateMerkleProof(entryIndex); 16 | 17 | const helper = provider.open( 18 | AirdropHelper.createFromConfig( 19 | { 20 | airdrop: Address.parse('EQAGUXoAPHIHYleSbSE05egNAlK8YAaYqUQsMho709gMBXU2'), 21 | index: entryIndex, 22 | proofHash: proof.hash(), 23 | }, 24 | await compile('AirdropHelper') 25 | ) 26 | ); 27 | 28 | if (!(await provider.isContractDeployed(helper.address))) { 29 | await helper.sendDeploy(provider.sender()); 30 | await provider.waitForDeploy(helper.address); 31 | } 32 | 33 | await helper.sendClaim(123n, proof); // 123 -> any query_id 34 | } 35 | -------------------------------------------------------------------------------- /scripts/deployAirdrop.ts: -------------------------------------------------------------------------------- 1 | import { Address, beginCell, toNano } from '@ton/core'; 2 | import { Airdrop, AirdropEntry, generateEntriesDictionary } from '../wrappers/Airdrop'; 3 | import { compile, NetworkProvider } from '@ton/blueprint'; 4 | import { JettonMinter } from '../wrappers/JettonMinter'; 5 | 6 | export async function run(provider: NetworkProvider) { 7 | const entries: AirdropEntry[] = [ 8 | { 9 | address: Address.parse('EQBKgXCNLPexWhs2L79kiARR1phGH1LwXxRbNsCFF9doc2lN'), 10 | amount: toNano('1'), 11 | }, 12 | { 13 | address: Address.parse('EQBIhPuWmjT7fP-VomuTWseE8JNWv2q7QYfsVQ1IZwnMk8wL'), 14 | amount: toNano('2'), 15 | }, 16 | { 17 | address: Address.parse('EQB4cwGljhouzFwc6EHpCacCtsK7_XIj-tNfM5udgW6IxO9R'), 18 | amount: toNano('1.5'), 19 | }, 20 | ]; 21 | 22 | const dict = generateEntriesDictionary(entries); 23 | const dictCell = beginCell().storeDictDirect(dict).endCell(); 24 | console.log(`Dictionary cell (store it somewhere on your backend: ${dictCell.toBoc().toString('base64')}`); 25 | const merkleRoot = BigInt('0x' + dictCell.hash().toString('hex')); 26 | 27 | const jettonMinterAddress = Address.parse('EQD0vdSA_NedR9uvbgN9EikRX-suesDxGeFg69XQMavfLqIw'); 28 | const jettonMinter = provider.open(JettonMinter.createFromAddress(jettonMinterAddress)); 29 | 30 | const airdrop = provider.open( 31 | Airdrop.createFromConfig( 32 | { 33 | merkleRoot, 34 | helperCode: await compile('AirdropHelper'), 35 | }, 36 | await compile('Airdrop') 37 | ) 38 | ); 39 | 40 | await airdrop.sendDeploy(provider.sender(), toNano('0.05'), await jettonMinter.getWalletAddressOf(airdrop.address)); 41 | 42 | await provider.waitForDeploy(airdrop.address); 43 | 44 | // run methods on `airdrop` 45 | } 46 | -------------------------------------------------------------------------------- /tests/Airdrop.spec.ts: -------------------------------------------------------------------------------- 1 | import { Blockchain, SandboxContract, TreasuryContract, printTransactionFees } from '@ton/sandbox'; 2 | import { Cell, Dictionary, beginCell, toNano } from '@ton/core'; 3 | import { Airdrop, AirdropEntry, generateEntriesDictionary } from '../wrappers/Airdrop'; 4 | import '@ton/test-utils'; 5 | import { compile } from '@ton/blueprint'; 6 | import { JettonMinter } from '../wrappers/JettonMinter'; 7 | import { JettonWallet } from '../wrappers/JettonWallet'; 8 | import { AirdropHelper } from '../wrappers/AirdropHelper'; 9 | 10 | describe('Airdrop', () => { 11 | let code: Cell; 12 | let codeHelper: Cell; 13 | let codeJettonMinter: Cell; 14 | let codeJettonWallet: Cell; 15 | 16 | beforeAll(async () => { 17 | code = await compile('Airdrop'); 18 | codeHelper = await compile('AirdropHelper'); 19 | codeJettonMinter = await compile('JettonMinter'); 20 | codeJettonWallet = await compile('JettonWallet'); 21 | }); 22 | 23 | let blockchain: Blockchain; 24 | let airdrop: SandboxContract; 25 | let dictionary: Dictionary; 26 | let dictCell: Cell; 27 | let users: SandboxContract[]; 28 | let jettonMinter: SandboxContract; 29 | let entries: AirdropEntry[]; 30 | 31 | beforeEach(async () => { 32 | blockchain = await Blockchain.create(); 33 | blockchain.now = 1000; 34 | 35 | users = await blockchain.createWallets(1000); 36 | 37 | entries = []; 38 | for (let i = 0; i < 1000; i++) { 39 | entries.push({ 40 | address: users[parseInt(i.toString())].address, 41 | amount: BigInt(Math.floor(Math.random() * 1e9)), 42 | }); 43 | } 44 | dictionary = generateEntriesDictionary(entries); 45 | 46 | dictCell = beginCell().storeDictDirect(dictionary).endCell(); 47 | 48 | jettonMinter = blockchain.openContract( 49 | JettonMinter.createFromConfig( 50 | { 51 | walletCode: codeJettonWallet, 52 | admin: users[0].address, 53 | content: Cell.EMPTY, 54 | }, 55 | codeJettonMinter 56 | ) 57 | ); 58 | 59 | await jettonMinter.sendDeploy(users[0].getSender(), toNano('0.05')); 60 | 61 | airdrop = blockchain.openContract( 62 | Airdrop.createFromConfig( 63 | { 64 | helperCode: codeHelper, 65 | merkleRoot: BigInt('0x' + dictCell.hash().toString('hex')), 66 | begin: 1100, 67 | admin: users[0].address, 68 | }, 69 | code 70 | ) 71 | ); 72 | 73 | const deployResult = await airdrop.sendDeploy( 74 | users[0].getSender(), 75 | toNano('0.05'), 76 | await jettonMinter.getWalletAddressOf(airdrop.address) 77 | ); 78 | 79 | expect(deployResult.transactions).toHaveTransaction({ 80 | from: users[0].address, 81 | to: airdrop.address, 82 | deploy: true, 83 | success: true, 84 | }); 85 | 86 | await jettonMinter.sendMint( 87 | users[0].getSender(), 88 | toNano('0.05'), 89 | toNano('0.01'), 90 | airdrop.address, 91 | toNano('1000000') 92 | ); 93 | }); 94 | 95 | it('should deploy', async () => { 96 | // the check is done inside beforeEach 97 | // blockchain and airdrop are ready to use 98 | }); 99 | 100 | it('should not claim until begin', async () => { 101 | const merkleProof = dictionary.generateMerkleProof(1n); 102 | const helper = blockchain.openContract( 103 | AirdropHelper.createFromConfig( 104 | { 105 | airdrop: airdrop.address, 106 | index: 1n, 107 | proofHash: merkleProof.hash(), 108 | }, 109 | codeHelper 110 | ) 111 | ); 112 | await helper.sendDeploy(users[1].getSender()); 113 | const result = await helper.sendClaim(123n, merkleProof); 114 | expect(result.transactions).toHaveTransaction({ 115 | on: airdrop.address, 116 | success: false, 117 | exitCode: 708, 118 | }); 119 | expect(await helper.getClaimed()).toBeFalsy(); 120 | }); 121 | 122 | it('should allow admin to withdraw rewards before begin', async () => { 123 | { 124 | const result = await airdrop.sendWithdrawJettons(users[0].getSender(), toNano('0.1'), toNano('1000')); 125 | expect(result.transactions).toHaveTransaction({ 126 | on: airdrop.address, 127 | success: true, 128 | }); 129 | expect( 130 | await blockchain 131 | .openContract( 132 | JettonWallet.createFromAddress(await jettonMinter.getWalletAddressOf(users[0].address)) 133 | ) 134 | .getJettonBalance() 135 | ).toEqual(toNano('1000')); 136 | } 137 | 138 | blockchain.now = 1100; 139 | 140 | { 141 | const result = await airdrop.sendWithdrawJettons(users[0].getSender(), toNano('0.1'), toNano('1000')); 142 | expect(result.transactions).toHaveTransaction({ 143 | on: airdrop.address, 144 | success: false, 145 | exitCode: 708, 146 | }); 147 | } 148 | }); 149 | 150 | it('should claim one time', async () => { 151 | blockchain.now = 2000; 152 | 153 | const merkleProof = dictionary.generateMerkleProof(1n); 154 | const helper = blockchain.openContract( 155 | AirdropHelper.createFromConfig( 156 | { 157 | airdrop: airdrop.address, 158 | index: 1n, 159 | proofHash: merkleProof.hash(), 160 | }, 161 | codeHelper 162 | ) 163 | ); 164 | await helper.sendDeploy(users[1].getSender()); 165 | const result = await helper.sendClaim(123n, merkleProof); 166 | expect(result.transactions).toHaveTransaction({ 167 | on: airdrop.address, 168 | success: true, 169 | }); 170 | expect( 171 | await blockchain 172 | .openContract(JettonWallet.createFromAddress(await jettonMinter.getWalletAddressOf(users[1].address))) 173 | .getJettonBalance() 174 | ).toEqual(dictionary.get(1n)?.amount); 175 | expect(await helper.getClaimed()).toBeTruthy(); 176 | }); 177 | 178 | it('should claim many times', async () => { 179 | blockchain.now = 2000; 180 | 181 | for (let i = 0; i < 1000; i += 1 + Math.floor(Math.random() * 25)) { 182 | const merkleProof = dictionary.generateMerkleProof(BigInt(i)); 183 | const helper = blockchain.openContract( 184 | AirdropHelper.createFromConfig( 185 | { 186 | airdrop: airdrop.address, 187 | index: BigInt(i), 188 | proofHash: merkleProof.hash(), 189 | }, 190 | codeHelper 191 | ) 192 | ); 193 | await helper.sendDeploy(users[i].getSender()); 194 | const result = await helper.sendClaim(123n, merkleProof); 195 | expect(result.transactions).toHaveTransaction({ 196 | on: airdrop.address, 197 | success: true, 198 | }); 199 | expect( 200 | await blockchain 201 | .openContract( 202 | JettonWallet.createFromAddress(await jettonMinter.getWalletAddressOf(users[i].address)) 203 | ) 204 | .getJettonBalance() 205 | ).toEqual(dictionary.get(BigInt(i))?.amount); 206 | expect(await helper.getClaimed()).toBeTruthy(); 207 | } 208 | }); 209 | 210 | it('should not claim if already did', async () => { 211 | blockchain.now = 2000; 212 | 213 | const merkleProof = dictionary.generateMerkleProof(1n); 214 | 215 | const helper = blockchain.openContract( 216 | AirdropHelper.createFromConfig( 217 | { 218 | airdrop: airdrop.address, 219 | index: 1n, 220 | proofHash: merkleProof.hash(), 221 | }, 222 | codeHelper 223 | ) 224 | ); 225 | await helper.sendDeploy(users[1].getSender()); 226 | 227 | { 228 | const result = await helper.sendClaim(123n, merkleProof); 229 | expect(result.transactions).toHaveTransaction({ 230 | on: airdrop.address, 231 | success: true, 232 | }); 233 | expect( 234 | await blockchain 235 | .openContract( 236 | JettonWallet.createFromAddress(await jettonMinter.getWalletAddressOf(users[1].address)) 237 | ) 238 | .getJettonBalance() 239 | ).toEqual(dictionary.get(1n)?.amount); 240 | expect(await helper.getClaimed()).toBeTruthy(); 241 | } 242 | 243 | { 244 | await expect(helper.sendClaim(123n, merkleProof)).rejects.toThrow(); 245 | expect( 246 | await blockchain 247 | .openContract( 248 | JettonWallet.createFromAddress(await jettonMinter.getWalletAddressOf(users[1].address)) 249 | ) 250 | .getJettonBalance() 251 | ).toEqual(dictionary.get(1n)?.amount); 252 | expect(await helper.getClaimed()).toBeTruthy(); 253 | } 254 | 255 | { 256 | await expect(helper.sendClaim(123n, merkleProof)).rejects.toThrow(); 257 | expect( 258 | await blockchain 259 | .openContract( 260 | JettonWallet.createFromAddress(await jettonMinter.getWalletAddressOf(users[1].address)) 261 | ) 262 | .getJettonBalance() 263 | ).toEqual(dictionary.get(1n)?.amount); 264 | expect(await helper.getClaimed()).toBeTruthy(); 265 | } 266 | }); 267 | 268 | it('should not claim with wrong index', async () => { 269 | blockchain.now = 2000; 270 | 271 | { 272 | const merkleProof = dictionary.generateMerkleProof(2n); 273 | const helper = blockchain.openContract( 274 | AirdropHelper.createFromConfig( 275 | { 276 | airdrop: airdrop.address, 277 | index: 1n, 278 | proofHash: merkleProof.hash(), 279 | }, 280 | codeHelper 281 | ) 282 | ); 283 | await helper.sendDeploy(users[1].getSender()); 284 | const result = await helper.sendClaim(123n, merkleProof); 285 | expect(result.transactions).toHaveTransaction({ 286 | from: helper.address, 287 | to: airdrop.address, 288 | success: false, 289 | }); 290 | expect( 291 | await blockchain 292 | .openContract( 293 | JettonWallet.createFromAddress(await jettonMinter.getWalletAddressOf(users[1].address)) 294 | ) 295 | .getJettonBalance() 296 | ).toEqual(0n); 297 | } 298 | 299 | { 300 | const merkleProof = dictionary.generateMerkleProof(1n); 301 | const helper = blockchain.openContract( 302 | AirdropHelper.createFromConfig( 303 | { 304 | airdrop: airdrop.address, 305 | index: 1n, 306 | proofHash: merkleProof.hash(), 307 | }, 308 | codeHelper 309 | ) 310 | ); 311 | await helper.sendDeploy(users[1].getSender()); 312 | const result = await helper.sendClaim(123n, merkleProof); 313 | expect(result.transactions).toHaveTransaction({ 314 | from: helper.address, 315 | to: airdrop.address, 316 | success: true, 317 | }); 318 | expect( 319 | await blockchain 320 | .openContract( 321 | JettonWallet.createFromAddress(await jettonMinter.getWalletAddressOf(users[1].address)) 322 | ) 323 | .getJettonBalance() 324 | ).toEqual(dictionary.get(1n)?.amount); 325 | expect(await helper.getClaimed()).toBeTruthy(); 326 | } 327 | }); 328 | }); 329 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "outDir": "dist", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /wrappers/Airdrop.compile.ts: -------------------------------------------------------------------------------- 1 | import { CompilerConfig } from '@ton/blueprint'; 2 | 3 | export const compile: CompilerConfig = { 4 | lang: 'func', 5 | targets: ['contracts/airdrop.fc'], 6 | }; 7 | -------------------------------------------------------------------------------- /wrappers/Airdrop.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Dictionary, 3 | Address, 4 | beginCell, 5 | Cell, 6 | Contract, 7 | contractAddress, 8 | ContractProvider, 9 | Sender, 10 | SendMode, 11 | Builder, 12 | Slice, 13 | } from '@ton/core'; 14 | 15 | export type AirdropConfig = { 16 | merkleRoot: bigint; 17 | helperCode: Cell; 18 | begin: number; 19 | admin: Address; 20 | }; 21 | 22 | export function airdropConfigToCell(config: AirdropConfig): Cell { 23 | return beginCell() 24 | .storeUint(0, 2) 25 | .storeUint(config.merkleRoot, 256) 26 | .storeRef(config.helperCode) 27 | .storeUint(config.begin, 64) 28 | .storeAddress(config.admin) 29 | .storeUint(Math.floor(Math.random() * 1e9), 64) 30 | .endCell(); 31 | } 32 | 33 | export type AirdropEntry = { 34 | address: Address; 35 | amount: bigint; 36 | }; 37 | 38 | export const airdropEntryValue = { 39 | serialize: (src: AirdropEntry, buidler: Builder) => { 40 | buidler.storeAddress(src.address).storeCoins(src.amount); 41 | }, 42 | parse: (src: Slice) => { 43 | return { 44 | address: src.loadAddress(), 45 | amount: src.loadCoins(), 46 | }; 47 | }, 48 | }; 49 | 50 | export function generateEntriesDictionary(entries: AirdropEntry[]): Dictionary { 51 | let dict: Dictionary = Dictionary.empty(Dictionary.Keys.BigUint(256), airdropEntryValue); 52 | 53 | for (let i = 0; i < entries.length; i++) { 54 | dict.set(BigInt(i), entries[i]); 55 | } 56 | 57 | return dict; 58 | } 59 | 60 | export class Airdrop implements Contract { 61 | constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) {} 62 | 63 | static createFromAddress(address: Address) { 64 | return new Airdrop(address); 65 | } 66 | 67 | static createFromConfig(config: AirdropConfig, code: Cell, workchain = 0) { 68 | const data = airdropConfigToCell(config); 69 | const init = { code, data }; 70 | return new Airdrop(contractAddress(workchain, init), init); 71 | } 72 | 73 | async sendDeploy(provider: ContractProvider, via: Sender, value: bigint, jettonWallet: Address) { 74 | await provider.internal(via, { 75 | value, 76 | sendMode: SendMode.PAY_GAS_SEPARATELY, 77 | body: beginCell().storeUint(0x610ca46c, 32).storeUint(0, 64).storeAddress(jettonWallet).endCell(), 78 | }); 79 | } 80 | 81 | async sendWithdrawJettons(provider: ContractProvider, via: Sender, value: bigint, amount: bigint) { 82 | await provider.internal(via, { 83 | value, 84 | sendMode: SendMode.PAY_GAS_SEPARATELY, 85 | body: beginCell().storeUint(0x190592b2, 32).storeUint(0, 64).storeCoins(amount).endCell(), 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /wrappers/AirdropHelper.compile.ts: -------------------------------------------------------------------------------- 1 | import { CompilerConfig } from '@ton/blueprint'; 2 | 3 | export const compile: CompilerConfig = { 4 | lang: 'func', 5 | targets: ['contracts/airdrop_helper.fc'], 6 | }; 7 | -------------------------------------------------------------------------------- /wrappers/AirdropHelper.ts: -------------------------------------------------------------------------------- 1 | import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, toNano } from '@ton/core'; 2 | import { AirdropEntry } from './Airdrop'; 3 | 4 | export type AirdropHelperConfig = { 5 | airdrop: Address; 6 | proofHash: Buffer; 7 | index: bigint; 8 | }; 9 | 10 | export function airdropHelperConfigToCell(config: AirdropHelperConfig): Cell { 11 | return beginCell() 12 | .storeBit(false) 13 | .storeAddress(config.airdrop) 14 | .storeBuffer(config.proofHash, 32) 15 | .storeUint(config.index, 256) 16 | .endCell(); 17 | } 18 | 19 | export class AirdropHelper implements Contract { 20 | constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) {} 21 | 22 | static createFromAddress(address: Address) { 23 | return new AirdropHelper(address); 24 | } 25 | 26 | static createFromConfig(config: AirdropHelperConfig, code: Cell, workchain = 0) { 27 | const data = airdropHelperConfigToCell(config); 28 | const init = { code, data }; 29 | return new AirdropHelper(contractAddress(workchain, init), init); 30 | } 31 | 32 | async sendDeploy(provider: ContractProvider, via: Sender) { 33 | await provider.internal(via, { 34 | value: toNano('0.15'), 35 | }); 36 | } 37 | 38 | async sendClaim(provider: ContractProvider, queryId: bigint, proof: Cell) { 39 | await provider.external(beginCell().storeUint(queryId, 64).storeRef(proof).endCell()); 40 | } 41 | 42 | async getClaimed(provider: ContractProvider): Promise { 43 | if ((await provider.getState()).state.type == 'uninit') { 44 | return false; 45 | } 46 | const stack = (await provider.get('get_claimed', [])).stack; 47 | return stack.readBoolean(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /wrappers/JettonMinter.compile.ts: -------------------------------------------------------------------------------- 1 | import { CompilerConfig } from '@ton/blueprint'; 2 | 3 | export const compile: CompilerConfig = { 4 | targets: ['contracts/jetton/jetton_minter.fc'], 5 | }; 6 | -------------------------------------------------------------------------------- /wrappers/JettonMinter.ts: -------------------------------------------------------------------------------- 1 | import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode } from '@ton/core'; 2 | 3 | export type JettonMinterConfig = { 4 | admin: Address; 5 | content: Cell; 6 | walletCode: Cell; 7 | }; 8 | 9 | export function jettonMinterConfigToCell(config: JettonMinterConfig): Cell { 10 | return beginCell() 11 | .storeCoins(0) 12 | .storeAddress(config.admin) 13 | .storeRef(config.content) 14 | .storeRef(config.walletCode) 15 | .endCell(); 16 | } 17 | 18 | export class JettonMinter implements Contract { 19 | constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) {} 20 | 21 | static createFromAddress(address: Address) { 22 | return new JettonMinter(address); 23 | } 24 | 25 | static createFromConfig(config: JettonMinterConfig, code: Cell, workchain = 0) { 26 | const data = jettonMinterConfigToCell(config); 27 | const init = { code, data }; 28 | return new JettonMinter(contractAddress(workchain, init), init); 29 | } 30 | 31 | async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { 32 | await provider.internal(via, { 33 | value, 34 | sendMode: SendMode.PAY_GAS_SEPARATELY, 35 | body: beginCell().endCell(), 36 | }); 37 | } 38 | 39 | async sendMint( 40 | provider: ContractProvider, 41 | via: Sender, 42 | value: bigint, 43 | forwardValue: bigint, 44 | recipient: Address, 45 | amount: bigint 46 | ) { 47 | await provider.internal(via, { 48 | sendMode: SendMode.PAY_GAS_SEPARATELY, 49 | body: beginCell() 50 | .storeUint(21, 32) 51 | .storeUint(0, 64) 52 | .storeAddress(recipient) 53 | .storeCoins(forwardValue) 54 | .storeRef( 55 | beginCell() 56 | .storeUint(0x178d4519, 32) 57 | .storeUint(0, 64) 58 | .storeCoins(amount) 59 | .storeAddress(this.address) 60 | .storeAddress(this.address) 61 | .storeCoins(0) 62 | .storeUint(0, 1) 63 | .endCell() 64 | ) 65 | .endCell(), 66 | value: value + forwardValue, 67 | }); 68 | } 69 | 70 | async getWalletAddressOf(provider: ContractProvider, address: Address) { 71 | return ( 72 | await provider.get('get_wallet_address', [ 73 | { type: 'slice', cell: beginCell().storeAddress(address).endCell() }, 74 | ]) 75 | ).stack.readAddress(); 76 | } 77 | 78 | async getWalletCode(provider: ContractProvider) { 79 | let stack = (await provider.get('get_jetton_data', [])).stack; 80 | stack.skip(4); 81 | return stack.readCell(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /wrappers/JettonWallet.compile.ts: -------------------------------------------------------------------------------- 1 | import { CompilerConfig } from '@ton/blueprint'; 2 | 3 | export const compile: CompilerConfig = { 4 | targets: ['contracts/jetton/jetton_wallet.fc'], 5 | }; 6 | -------------------------------------------------------------------------------- /wrappers/JettonWallet.ts: -------------------------------------------------------------------------------- 1 | import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode } from '@ton/core'; 2 | 3 | export type JettonWalletConfig = { 4 | owner: Address; 5 | minter: Address; 6 | walletCode: Cell; 7 | }; 8 | 9 | export function jettonWalletConfigToCell(config: JettonWalletConfig): Cell { 10 | return beginCell() 11 | .storeCoins(0) 12 | .storeAddress(config.owner) 13 | .storeAddress(config.minter) 14 | .storeRef(config.walletCode) 15 | .endCell(); 16 | } 17 | 18 | export class JettonWallet implements Contract { 19 | constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) {} 20 | 21 | static createFromAddress(address: Address) { 22 | return new JettonWallet(address); 23 | } 24 | 25 | static createFromConfig(config: JettonWalletConfig, code: Cell, workchain = 0) { 26 | const data = jettonWalletConfigToCell(config); 27 | const init = { code, data }; 28 | return new JettonWallet(contractAddress(workchain, init), init); 29 | } 30 | 31 | async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { 32 | await provider.internal(via, { 33 | value, 34 | sendMode: SendMode.PAY_GAS_SEPARATELY, 35 | body: beginCell().endCell(), 36 | }); 37 | } 38 | 39 | async sendTransfer( 40 | provider: ContractProvider, 41 | via: Sender, 42 | value: bigint, 43 | forwardValue: bigint, 44 | recipient: Address, 45 | amount: bigint, 46 | forwardPayload: Cell 47 | ) { 48 | await provider.internal(via, { 49 | sendMode: SendMode.PAY_GAS_SEPARATELY, 50 | body: beginCell() 51 | .storeUint(0x0f8a7ea5, 32) 52 | .storeUint(0, 64) 53 | .storeCoins(amount) 54 | .storeAddress(recipient) 55 | .storeAddress(via.address) 56 | .storeUint(0, 1) 57 | .storeCoins(forwardValue) 58 | .storeUint(1, 1) 59 | .storeRef(forwardPayload) 60 | .endCell(), 61 | value: value + forwardValue, 62 | }); 63 | } 64 | 65 | async getJettonBalance(provider: ContractProvider) { 66 | let state = await provider.getState(); 67 | if (state.state.type !== 'active') { 68 | return 0n; 69 | } 70 | let res = await provider.get('get_wallet_data', []); 71 | return res.stack.readBigNumber(); 72 | } 73 | } 74 | --------------------------------------------------------------------------------