├── .gitignore ├── LICENSE ├── README.md ├── audits ├── Vesting REP-final-20220805T101405Z.pdf └── Vesting Wallet - Zellic Audit Report - final.pdf ├── build └── VestingWallet.compiled.json ├── contracts ├── imports │ └── stdlib.fc └── vesting_wallet.fc ├── instruction.md ├── jest.config.ts ├── package-lock.json ├── package.json ├── scripts └── deployVestingWallet.ts ├── tests └── VestingWallet.spec.ts ├── tsconfig.json └── wrappers ├── VestingWallet.compile.ts └── VestingWallet.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 TON - The Open Network 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 | # Vesting contract 2 | 3 | This contract allows you to lock a certain amount of Toncoin for a specified time and gradually unlock them. 4 | 5 | ## Vesting parameters 6 | 7 | Vesting parameters are unchanged and is set during deployment. 8 | 9 | `vesting_total_amount` - in nanotons, the total amount of locked Toncoins. 10 | 11 | `vesting_start_time` - unixtime, the starting point of the vesting period, until this moment the `vesting_total_amount` is locked, after that it starts to unlock according to other parameters. 12 | 13 | `vesting_total_duration` - total vesting duration in seconds (e.g. `31104000` for one year). 14 | 15 | `unlock_period` - unlock period in seconds (e.g. `2592000` for once a month). 16 | 17 | `cliff_duration` - starting cliff period in seconds (e.g. `5184000` for 2 months). 18 | 19 | `vesting_sender_address` - the address to which you can return the Toncoins (even if they are locked) at any time; also this address can append the whitelist. 20 | 21 | `owner_address` - the one to whom the vesting was issued, from this address, he can initiate the sending of Toncoins from the vesting contract. 22 | 23 | You can get this parameters by `get_vesting_data()` get-method. 24 | 25 | The parameters must satisfy the following conditions: 26 | 27 | ``` 28 | vesting_total_duration > 0 29 | vesting_total_duration <= 135 years (2^32 seconds) 30 | unlock_period > 0 31 | unlock_period <= vesting_total_duration 32 | cliff_duration >= 0 33 | cliff_duration < vesting_total_duration 34 | vesting_total_duration mod unlock_period == 0 35 | cliff_duration mod unlock_period == 0 36 | ``` 37 | 38 | Although the smart contract does not check for compliance with these conditions, after the contract is deployed and before sending Toncoins to it, the user can verify that all parameters are OK by get-method. 39 | 40 | ## Lock 41 | 42 | Before the `vesting_start_time`, all `vesting_total_amount` are locked. 43 | 44 | Starting from `vesting_start_time` the amount starts to unlock proportionately. 45 | 46 | For example if `vesting_total_duration` is 10 months, and `unlock_period` is 1 month, and `vesting_total_amount` is 500 TON then every month will unlock 500*(10/100)=50 TON, and in 10 months all 500 TON will be unlocked. 47 | 48 | If there is a cliff period, nothing is unlocked during this cliff period, and after it has passed, the amount is unlocked according to the formula above. 49 | 50 | For example if `cliff_period` is 3 months, and the other parameters are the same as in the previous example, then first 3 months nothing will be unlocked and on 3 months 150 TON will be unlocked at once (and then 50 TON every month). 51 | 52 | Get-method `get_locked_amount(int at_time)` allows you to calculate how much will be locked at a certain point in time. 53 | 54 | You can only send the locked Toncoins to the whitelist addresses or `vesting_sender_address`. 55 | 56 | You can send the unlocked Toncoins whenever and wherever you like. 57 | 58 | ## Whitelist 59 | 60 | Whitelist is a list of addresses to which you can send Toncoins, even if coins are still locked. 61 | 62 | Get-method `get_whitelist()` returns all whitelist addresses as list of (wc, hash_part) tuples. 63 | 64 | Get-method `is_whitelisted(slice address)` checks to see if this address is on the whitelist. 65 | 66 | `vesting_sender_address` can append new addresses to whitelist at any time by `op::add_whitelist` message. 67 | 68 | Unable to remove an address from the whitelist. 69 | 70 | Also, locked coins can always be sent to the `vesting_sender_address` (it doesn't need to be added to the whitelist separately). 71 | 72 | ## Top-up 73 | 74 | You can send Toncoins to vesting contract from any address. 75 | 76 | ## Wallet smart contract 77 | 78 | This contract is designed similar to the [standard wallet V3 smart contract](https://github.com/ton-blockchain/ton/blob/master/crypto/smartcont/wallet3-code.fc). 79 | 80 | In his data, he keeps `seqno`, `subwallet_id`, `public_key` and accepts external messages of the same format. 81 | 82 | Get-methods `seqno()`, `get_subwallet_id()` and `get_public_key()` are available. 83 | 84 | Unlike a standard wallet, vesting contract allows you to send only one message at a time. 85 | 86 | ## Send 87 | 88 | The owner of the public key can initiate the sending of Toncoins from the vesting contract by an external message, just like in standard wallets. 89 | 90 | The Toncoin sending can also be initiated by an `op::send` internal message sent from the `owner_address`. 91 | 92 | In practice, both the public key and the `owner_address` are owned by the same user. 93 | 94 | ## Whitelist restrictions 95 | 96 | Messages that can be sent to the `vesting_sender_address` have the following restrictions: 97 | 98 | - only `send_mode == 3` allowed; 99 | 100 | 101 | In most cases, addresses are added to the whitelist to allow the user to validate using locked coins or to stake locked coins into the pools. 102 | 103 | To avoid theft of Toncoins, messages that can be sent to the whitelist have the following restrictions: 104 | 105 | - only `send_mode == 3` allowed; 106 | 107 | - only bounceable messages allowed; 108 | 109 | - no `state_init` attachment allowed; 110 | 111 | If destination is system elector address: 112 | 113 | - only `op::elector_new_stake`, `op::elector_recover_stake`, `op::vote_for_complaint`, `op::vote_for_proposal` operations allowed; 114 | 115 | If destination is system config address: 116 | 117 | - only `op::vote_for_proposal` operation allowed; 118 | 119 | For other destinations: 120 | 121 | - allowed empty messages and empty text messages; 122 | - allowed text messages starts only with "d", "w", "D", "W"; 123 | - allowed `op::single_nominator_pool_withdraw`, `op::single_nominator_pool_change_validator`, `op::ton_stakers_deposit`, `op::jetton_burn`, `op::ton_stakers_vote`, `op::vote_for_proposal`, `op::vote_for_complaint` operations; 124 | 125 | There are no restrictions on addresses not included in the whitelist. 126 | 127 | No restrictions apply when sending unlocked Toncoins, even if we send to whitelist or `vesting_sender_address`. 128 | 129 | ## Project structure 130 | 131 | - `contracts` - source code of all the smart contracts of the project and their dependencies. 132 | - `wrappers` - wrapper classes (implementing `Contract` from ton-core) for the contracts, including any [de]serialization primitives and compilation functions. 133 | - `tests` - tests for the contracts. 134 | - `scripts` - scripts used by the project, mainly the deployment scripts. 135 | 136 | ## How to use 137 | 138 | ### Build 139 | 140 | `npx blueprint build` or `yarn blueprint build` 141 | 142 | ### Test 143 | 144 | `npx blueprint test` or `yarn blueprint test` 145 | 146 | ### Deploy or run another script 147 | 148 | `npx blueprint run` or `yarn blueprint run` 149 | 150 | ### Add a new contract 151 | 152 | `npx blueprint create ContractName` or `yarn blueprint create ContractName` 153 | 154 | ## Security 155 | 156 | The vesting contract has been created by TON Core team and audited by security companies: 157 | 158 | - Zellic: [Audit Report](https://github.com/ton-blockchain/vesting-contract/blob/main/audits/Vesting%20Wallet%20-%20Zellic%20Audit%20Report%20-%20final.pdf) 159 | - CertiK: [Audit Report](https://github.com/ton-blockchain/vesting-contract/blob/main/audits/Vesting%20REP-final-20220805T101405Z.pdf) 160 | 161 | Feel free to review these reports for a detailed understanding of the contract's security measures. 162 | 163 | # License 164 | MIT 165 | -------------------------------------------------------------------------------- /audits/Vesting REP-final-20220805T101405Z.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-blockchain/vesting-contract/2a63cb96942332abf92ed8425b37645fe4f41f86/audits/Vesting REP-final-20220805T101405Z.pdf -------------------------------------------------------------------------------- /audits/Vesting Wallet - Zellic Audit Report - final.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-blockchain/vesting-contract/2a63cb96942332abf92ed8425b37645fe4f41f86/audits/Vesting Wallet - Zellic Audit Report - final.pdf -------------------------------------------------------------------------------- /build/VestingWallet.compiled.json: -------------------------------------------------------------------------------- 1 | {"hex":"b5ee9c7241021c010003fb000114ff00f4a413f4bcf2c80b01020120030203b8f28308d71820d31fd31fd31f02f823bbf264ed44d0d31fd31fd3ff305abaf2a15033baf2a202f9014033f910f2a3f800db3c20d74ac0018e99ed44ed45ed47915bed67ed65ed648e82db3ced41edf101f2ff9130e2f841a4f861db3c1b1413020148110402012008050201200706020db9846db3cdb3c81b1a0129ba462db3cf845f846f847f848f849f84af84bf84481b0201200e090201620d0a02016a0c0b012fa2c76cf3e9100723281f2fff2743e112040423d029be84c61b000fa25fb513435c2c7e014bad346d9e36fc22470d4080847a4937d29910ce6903e9ff9837812801b7810148987159f318401b02016e100f0019af1df6a26840106b90eb858fc00019adce76a26840206b90eb85ffc003acd06c2220d749c160915be001d0d3030171b0915be0fa4030db3c01d31fd33ff84b5240c705238210a7733acdbab08ea46c12db3c8210f7733acd01708018c8cb055004cf1623fa0213cb6acb1fcb3fc98040fb00e30e1b141201cef84a5240c7050382107258a69bba13b08ed18e2c01fa407fc8ca0002fa4401c8ca07cbffc9d0f8441023810108f441f86420d74ac200209501d430d001deb312e68210f258a69b32708018c8cb055004cf1623fa0213cb6acb1fcb3fc98040fb00db3c925f03e2130066f848f847f846f845c8cb3fcb1fcb1fcb1ff849fa02f84acf16f84bcf16c9f844f843f842f841c8cb1fcb1fcbfff400ccc9ed54025cd307d4d1f823db3c20c2008e9b22c003f2e06421d0d303fa4031fa40f84a5220c705b3925f04e30d9130e201fb001a1503fa21fa4401c8ca07cbffc9d0f844810108f40a6fa1318f5f330172b0c002f2e06501fa003171d721fa0031fa0031d33f31d31f31d30001c000f2e066d3000193d430d0de2171db3c8e2a31d31f302082104e73744bba21821047657424bab121821056744370bab1018210566f7465bab1f2e067e30e70925f03e220c200191716000e9372fb029130e202ea0170db3c8e6d20d749c2008e63d31f21c00022830bbab122811001bab122821047d54391bab1228210595f07bcbab122821069fb306cbab1228210566f7465bab122821056744370bab1f2e06701c00021d749c200b08e15d3073020c06421c077b121c044b101c057b1f2e0689130e29130e2e30d1918001ad31f308210566f7465baf2e067004401fa4401c3ff925b70e001f833206e925b70e0d020d7498307b9925b70e0d70bffba0060f845f846a05210bc923070e0f845f848a05210b99330f849e0f849f849f84513a1f847a904f846f847a9041023a984a1007eed44d0d31f01f861d31f01f862d3ff01f863f40401f864d401d0d33f01f865d31f01f866d31f01f867d31f01f868fa0001f869fa4001f86afa4001f86bd1d10a20c6a7"} -------------------------------------------------------------------------------- /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/vesting_wallet.fc: -------------------------------------------------------------------------------- 1 | #include "imports/stdlib.fc"; 2 | 3 | const int op::add_whitelist = 0x7258a69b; 4 | const int op::add_whitelist_response = 0xf258a69b; 5 | const int op::send = 0xa7733acd; 6 | const int op::send_response = 0xf7733acd; 7 | 8 | ;; https://github.com/ton-blockchain/ton/blob/master/crypto/smartcont/elector-code.fc 9 | ;; https://github.com/ton-blockchain/ton/blob/master/crypto/smartcont/config-code.fc 10 | const int op::elector_new_stake = 0x4e73744b; 11 | const int op::elector_recover_stake = 0x47657424; 12 | const int op::vote_for_complaint = 0x56744370; 13 | const int op::vote_for_proposal = 0x566f7465; 14 | 15 | ;; single-nominator-pool: empty message to deposit; 0x1000 to withdraw https://github.com/orbs-network/single-nominator/blob/main/contracts/single-nominator.fc 16 | const int op::single_nominator_pool_withdraw = 0x1000; 17 | const int op::single_nominator_pool_change_validator = 0x1001; 18 | 19 | ;; tonstakers.com: deposit to pool; burn, vote to jetton-wallet - https://ton-ls-protocol.gitbook.io/ton-liquid-staking-protocol/protocol-concept/message-processing 20 | const int op::ton_stakers_deposit = 0x47d54391; 21 | const int op::jetton_burn = 0x595f07bc; 22 | const int op::ton_stakers_vote = 0x69fb306c; 23 | 24 | const int error::expired = 36; 25 | const int error::invalid_seqno = 33; 26 | const int error::invalid_subwallet_id = 34; 27 | const int error::invalid_signature = 35; 28 | 29 | const int error::send_mode_not_allowed = 100; 30 | const int error::non_bounceable_not_allowed = 101; 31 | const int error::state_init_not_allowed = 102; 32 | const int error::comment_not_allowed = 103; 33 | const int error::symbols_not_allowed = 104; 34 | 35 | ;; https://github.com/ton-blockchain/ton/blob/d2b418bb703ed6ccd89b7d40f9f1e44686012014/crypto/block/block.tlb#L605 36 | const int config_id = 0; 37 | const int elector_id = 1; 38 | 39 | ;; data 40 | 41 | global int stored_seqno; 42 | global int stored_subwallet; 43 | global int public_key; 44 | 45 | global cell whitelist; 46 | 47 | global int vesting_start_time; 48 | global int vesting_total_duration; 49 | global int unlock_period; 50 | global int cliff_duration; 51 | global int vesting_total_amount; 52 | global slice vesting_sender_address; 53 | global slice owner_address; 54 | 55 | ;; CONDITIONS: 56 | ;; vesting_total_duration > 0 57 | ;; vesting_total_duration <= 135 years (2^32 seconds) 58 | ;; unlock_period > 0 59 | ;; unlock_period <= vesting_total_duration 60 | ;; cliff_duration >= 0 61 | ;; cliff_duration < vesting_total_duration 62 | ;; vesting_total_duration mod unlock_period == 0 63 | ;; cliff_duration mod unlock_period == 0 64 | 65 | () load_vesting_parameters(cell data) impure inline { 66 | slice ds = data.begin_parse(); 67 | vesting_start_time = ds~load_uint(64); 68 | vesting_total_duration = ds~load_uint(32); 69 | unlock_period = ds~load_uint(32); 70 | cliff_duration = ds~load_uint(32); 71 | vesting_total_amount = ds~load_coins(); 72 | vesting_sender_address = ds~load_msg_addr(); 73 | owner_address = ds~load_msg_addr(); 74 | ds.end_parse(); 75 | } 76 | 77 | cell pack_vesting_parameters() inline { 78 | return begin_cell() 79 | .store_uint(vesting_start_time, 64) 80 | .store_uint(vesting_total_duration, 32) 81 | .store_uint(unlock_period, 32) 82 | .store_uint(cliff_duration, 32) 83 | .store_coins(vesting_total_amount) ;; max 124 bits 84 | .store_slice(vesting_sender_address) ;; 267 bit 85 | .store_slice(owner_address) ;; 267 bit 86 | .end_cell(); 87 | } 88 | 89 | () load_data() impure inline_ref { 90 | slice ds = get_data().begin_parse(); 91 | stored_seqno = ds~load_uint(32); 92 | stored_subwallet = ds~load_uint(32); 93 | public_key = ds~load_uint(256); 94 | whitelist = ds~load_dict(); 95 | load_vesting_parameters(ds~load_ref()); 96 | ds.end_parse(); 97 | } 98 | 99 | () save_data() impure inline_ref { 100 | set_data( 101 | begin_cell() 102 | .store_uint(stored_seqno, 32) 103 | .store_uint(stored_subwallet, 32) 104 | .store_uint(public_key, 256) 105 | .store_dict(whitelist) 106 | .store_ref(pack_vesting_parameters()) 107 | .end_cell() 108 | ); 109 | } 110 | 111 | ;; messages utils 112 | 113 | const int BOUNCEABLE = 0x18; 114 | const int NON_BOUNCEABLE = 0x10; 115 | 116 | const int SEND_MODE_REGULAR = 0; 117 | const int SEND_MODE_PAY_FEES_SEPARETELY = 1; 118 | const int SEND_MODE_IGNORE_ERRORS = 2; 119 | const int SEND_MODE_DESTROY = 32; 120 | const int SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE = 64; 121 | const int SEND_MODE_CARRY_ALL_BALANCE = 128; 122 | 123 | () return_excess(slice to_address, int op, int query_id) impure inline { 124 | builder msg = begin_cell() 125 | .store_uint(BOUNCEABLE, 6) 126 | .store_slice(to_address) 127 | .store_coins(0) 128 | .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) 129 | .store_uint(op, 32) 130 | .store_uint(query_id, 64); 131 | 132 | send_raw_message(msg.end_cell(), SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); 133 | } 134 | 135 | int match_address_from_config(slice address, int config_id) inline_ref { 136 | (int address_wc, int address_hash) = parse_std_addr(address); 137 | if (address_wc != -1) { 138 | return 0; 139 | } 140 | cell config_cell = config_param(config_id); 141 | if (cell_null?(config_cell)) { 142 | return 0; 143 | } 144 | slice config_slice = config_cell.begin_parse(); 145 | if (config_slice.slice_bits() < 256) { 146 | return 0; 147 | } 148 | return address_hash == config_slice.preload_uint(256); 149 | } 150 | 151 | 152 | ;; address utils 153 | 154 | const int ADDRESS_SIZE = 264; ;; 256 + 8 155 | 156 | slice pack_address(slice address) inline { 157 | (int wc, int address_hash) = parse_std_addr(address); 158 | return begin_cell().store_int(wc, 8).store_uint(address_hash, 256).end_cell().begin_parse(); 159 | } 160 | 161 | (int, int) unpack_address(slice address) inline { 162 | int wc = address~load_int(8); 163 | int address_hash = address~load_uint(256); 164 | return (wc, address_hash); 165 | } 166 | 167 | (slice, int) dict_get?(cell dict, int key_len, slice index) asm(index dict key_len) "DICTGET" "NULLSWAPIFNOT"; 168 | 169 | int _is_whitelisted(slice address) inline { 170 | (_, int found) = whitelist.dict_get?(ADDRESS_SIZE, pack_address(address)); 171 | return found; 172 | } 173 | 174 | ;; locked 175 | 176 | int _get_locked_amount(int now_time) inline_ref { 177 | if (now_time > vesting_start_time + vesting_total_duration) { 178 | return 0; 179 | } 180 | 181 | if (now_time < vesting_start_time + cliff_duration) { 182 | return vesting_total_amount; 183 | } 184 | 185 | return vesting_total_amount - muldiv(vesting_total_amount, 186 | (now_time - vesting_start_time) / unlock_period, 187 | vesting_total_duration / unlock_period); 188 | } 189 | 190 | () send_message(slice in_msg_body) impure inline_ref { 191 | int send_mode = in_msg_body~load_uint(8); 192 | cell msg = in_msg_body~load_ref(); 193 | in_msg_body.end_parse(); ;; only 1 ref allowed 194 | 195 | int locked_amount = _get_locked_amount(now()); 196 | 197 | if (locked_amount > 0) { ;; if the vesting has expired, you can send any messages 198 | 199 | throw_unless(error::send_mode_not_allowed, send_mode == SEND_MODE_IGNORE_ERRORS + SEND_MODE_PAY_FEES_SEPARETELY); 200 | 201 | slice msg_cs = msg.begin_parse(); 202 | int flags = msg_cs~load_uint(4); 203 | slice sender_address = msg_cs~load_msg_addr(); ;; skip 204 | slice destination_address = msg_cs~load_msg_addr(); 205 | 206 | if (~ equal_slices(destination_address, vesting_sender_address)) { ;; can send to vesting_sender_address any message 207 | 208 | if (_is_whitelisted(destination_address)) { 209 | int is_bounceable = (flags & 2) == 2; 210 | throw_unless(error::non_bounceable_not_allowed, is_bounceable); 211 | 212 | msg_cs~load_coins(); ;; skip value 213 | msg_cs~skip_bits(1); ;; skip extracurrency collection 214 | msg_cs~load_coins(); ;; skip ihr_fee 215 | msg_cs~load_coins(); ;; skip fwd_fee 216 | msg_cs~load_uint(64); ;; skip createdLt 217 | msg_cs~load_uint(32); ;; skip createdAt 218 | int maybe_state_init = msg_cs~load_uint(1); 219 | throw_unless(error::state_init_not_allowed, maybe_state_init == 0); 220 | 221 | int maybe_body = msg_cs~load_uint(1); 222 | slice body = maybe_body ? msg_cs~load_ref().begin_parse() : msg_cs; 223 | 224 | if (match_address_from_config(destination_address, elector_id)) { ;; elector - direct validation 225 | 226 | int op = body~load_uint(32); 227 | throw_unless(error::comment_not_allowed, 228 | (op == op::elector_new_stake) | (op == op::elector_recover_stake) | (op == op::vote_for_complaint) | (op == op::vote_for_proposal)); 229 | 230 | } elseif (match_address_from_config(destination_address, config_id)) { ;; conifg - direct validation 231 | 232 | int op = body~load_uint(32); 233 | throw_unless(error::comment_not_allowed, 234 | (op == op::vote_for_proposal)); 235 | 236 | } elseif (body.slice_bits() > 0) { ;; empty message allowed for other destination (not elector) 237 | 238 | int op = body~load_uint(32); 239 | throw_unless(error::comment_not_allowed, 240 | (op == 0) | ;; text comment 241 | (op == op::single_nominator_pool_withdraw) | (op == op::single_nominator_pool_change_validator) | ;; single-nominator 242 | (op == op::ton_stakers_deposit) | (op == op::jetton_burn) | (op == op::ton_stakers_vote) | ;; tonstakers.com 243 | (op == op::vote_for_proposal) | (op == op::vote_for_complaint) ;; for future 244 | ;; https://app.bemo.finance/ - empty message to deposit; op::jetton_burn to withdraw with cooldown 245 | ); 246 | 247 | if ((op == 0) & (body.slice_bits() > 0)) { ;; empty text comment allowed 248 | int action = body~load_uint(8); 249 | throw_unless(error::symbols_not_allowed, 250 | (action == "d"u) | (action == "w"u) | ;; nominator-pool - https://github.com/ton-blockchain/nominator-pool 251 | (action == "D"u) | (action == "W"u) ;; whales pool - https://github.com/tonwhales/ton-nominators/tree/main/sources 252 | 253 | ); 254 | } 255 | } 256 | 257 | locked_amount = 0; 258 | } 259 | 260 | if (locked_amount > 0) { 261 | raw_reserve(locked_amount, 2); ;; mode 2 - at most `amount` nanotons. Bit +2 in y means that the external action does not fail if the specified amount cannot be reserved; instead, all remaining balance is reserved 262 | } 263 | } 264 | } 265 | 266 | send_raw_message(msg, send_mode); 267 | } 268 | 269 | ;; receive 270 | 271 | () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { 272 | if (in_msg_body.slice_bits() < 32 + 64) { ;; ignore simple transfers 273 | return (); 274 | } 275 | 276 | slice cs = in_msg_full.begin_parse(); 277 | int flags = cs~load_uint(4); 278 | if (flags & 1) { ;; ignore all bounced messages 279 | return (); 280 | } 281 | slice sender_address = cs~load_msg_addr(); 282 | 283 | load_data(); 284 | 285 | int op = in_msg_body~load_uint(32); 286 | int query_id = in_msg_body~load_uint(64); 287 | 288 | if (equal_slices(sender_address, owner_address) & (op == op::send)) { 289 | 290 | send_message(in_msg_body); 291 | 292 | return_excess(sender_address, op::send_response, query_id); 293 | 294 | } elseif (equal_slices(sender_address, vesting_sender_address) & (op == op::add_whitelist)) { 295 | 296 | slice ref_cs = in_msg_body; 297 | int has_refs = 0; 298 | do { 299 | slice whitelist_address = ref_cs~load_msg_addr(); 300 | whitelist~dict_set_builder(ADDRESS_SIZE, pack_address(whitelist_address), begin_cell().store_int(-1, 1)); 301 | 302 | has_refs = ref_cs.slice_refs() > 0; 303 | if (has_refs) { 304 | cell ref = ref_cs~load_ref(); 305 | ref_cs = ref.begin_parse(); 306 | } 307 | } until (~ has_refs); 308 | 309 | return_excess(sender_address, op::add_whitelist_response, query_id); 310 | 311 | save_data(); 312 | 313 | } 314 | 315 | ;; else just accept coins from anyone 316 | } 317 | 318 | ;; same with wallet-v3 https://github.com/ton-blockchain/ton/blob/master/crypto/smartcont/wallet3-code.fc#L15 319 | () recv_external(slice in_msg) impure { 320 | slice signature = in_msg~load_bits(512); 321 | slice cs = in_msg; 322 | (int msg_subwallet_id, int valid_until, int msg_seqno) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32)); 323 | throw_if(error::expired, valid_until <= now()); 324 | slice ds = get_data().begin_parse(); 325 | (int my_seqno, int my_subwallet_id, int my_public_key) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256)); 326 | throw_unless(error::invalid_seqno, msg_seqno == my_seqno); 327 | throw_unless(error::invalid_subwallet_id, msg_subwallet_id == my_subwallet_id); 328 | throw_unless(error::invalid_signature, check_signature(slice_hash(in_msg), signature, my_public_key)); 329 | 330 | accept_message(); 331 | 332 | load_data(); 333 | 334 | if (slice_refs(cs) == 1) { 335 | try { 336 | send_message(cs); 337 | } catch (x, y) { 338 | } 339 | } 340 | 341 | stored_seqno += 1; 342 | save_data(); 343 | } 344 | 345 | ;; get-methods 346 | 347 | ;; same with wallet-v3 and wallet-v4 348 | int seqno() method_id { 349 | return get_data().begin_parse().preload_uint(32); 350 | } 351 | 352 | ;; same with wallet-v4 https://github.com/ton-blockchain/wallet-contract/blob/main/func/wallet-v4-code.fc 353 | int get_subwallet_id() method_id { 354 | return get_data().begin_parse().skip_bits(32).preload_uint(32); 355 | } 356 | 357 | ;; same with wallet-v3 and wallet-v4 358 | int get_public_key() method_id { 359 | return get_data().begin_parse().skip_bits(64).preload_uint(256); 360 | } 361 | 362 | (int, int, int, int, int, slice, slice, cell) get_vesting_data() method_id { 363 | load_data(); 364 | 365 | return (vesting_start_time, vesting_total_duration, unlock_period, cliff_duration, vesting_total_amount, 366 | vesting_sender_address, owner_address, whitelist); 367 | } 368 | 369 | ;; same with wallet-v4 370 | int is_whitelisted(slice address) method_id { 371 | load_data(); 372 | 373 | return _is_whitelisted(address); 374 | } 375 | 376 | ;; same with wallet-v4 377 | tuple get_whitelist() method_id { 378 | load_data(); 379 | 380 | var list = null(); 381 | 382 | cell d = whitelist; 383 | do { 384 | (d, slice key, slice value, int found) = d.dict_delete_get_min(ADDRESS_SIZE); 385 | if (found) { 386 | (int wc, int address_hash) = unpack_address(key); 387 | list = cons(pair(wc, address_hash), list); 388 | } 389 | } until (~ found); 390 | 391 | return list; 392 | } 393 | 394 | int get_locked_amount(int at_time) method_id { 395 | load_data(); 396 | 397 | return _get_locked_amount(at_time); 398 | } -------------------------------------------------------------------------------- /instruction.md: -------------------------------------------------------------------------------- 1 | # Vesting Instructions 2 | 3 | ## 1. User Wallet 4 | 5 | The vesting sender asks the user (vesting recipient) for their TON wallet address to allocate the vesting. 6 | 7 | If the user has not yet performed any outgoing transactions from this wallet (the wallet is not deployed), the sender will transfer 1 TON to the user and ask the user to send the 1 TON back. 8 | 9 | This process ensures that the user deploys their wallet and verifies their access to it. 10 | 11 | ## 2. Creating a Vesting Wallet 12 | 13 | The vesting sender visits https://vesting.ton.org/, enters the user’s wallet address in the “Address” field, and selects the “Create new vesting for this user” button. 14 | 15 | 16 | They must provide the following vesting details: 17 | 18 | * Vesting start date - choose a deferred date for lock up without accumulation of vesting before the date; 19 | 20 | * Total amount of vesting in TON; 21 | 22 | * Total vesting duration in days (including Cliff) - i.e. 760 days for 2 years vesting; 23 | 24 | * Cliff duration in days (zero if no cliff is needed) - period after the vesting starts when vesting accumulates but can not be withdrawn; all the accumulated amount will be available for withdrawal once the cliff period ends; 25 | 26 | * Unlocking frequency in days (equal to the total vesting duration if partial unlocking is not required) - i.e. 30 days for monthly vesting; 27 | 28 | * If direct validation from the vesting wallet is required, the “In masterchain” option must be checked; otherwise, it should be left unchecked; 29 | 30 | * Whitelist address(es) (if any) - i.e. single nominator smart contracts addresses. 31 | 32 | The total vesting duration must be divisible by the unlocking frequency. 33 | 34 | The Cliff period should also be divisible by the unlocking frequency. 35 | 36 | By selecting the “Create” button, you will generate a vesting wallet contract at a cost of 0.5 TON. 37 | 38 | 39 | Shortly afterwards, the vesting wallet page will open. The sender verifies that all parameters are correct. 40 | 41 | 42 | The sender will then share the link with the user so that they can confirm the parameters are correct by viewing them at https://vesting.ton.org/. 43 | 44 | ## 3. Topping Up the Vesting Wallet 45 | 46 | The vesting wallet balance can be topped up from any wallet. 47 | 48 | 49 | ## 4. Vesting Sender 50 | 51 | The address from which the vesting wallet was created has several properties: 52 | 53 | * New whitelist addresses for this vesting can be added at any time (removing whitelist addresses is not possible). 54 | 55 | * The user can send (return) coins from this address at any time, even if the coins are still locked in the vesting. 56 | 57 | Therefore, in the event of a breach of the sender's wallet, the vesting can be unlocked. Hence, when issuing a large vesting, it is advisable to do so from different wallets. 58 | 59 | 60 | ## 5. Wallet Usage 61 | 62 | The user can utilize unlocked coins from the vesting wallet. 63 | 64 | 65 | To do this, they can open the vesting wallet at https://vesting.ton.org/ and click the “Send” button. In this case, the user must log in to https://vesting.ton.org/ with the wallet to which the vesting was granted. 66 | 67 | In the future, native support for vesting wallets in Tonkeeper or TonHub may become possible. 68 | 69 | 70 | ## 6. Whitelist 71 | 72 | The vesting sender (the one who created the vesting contract) can add any addresses to the vesting wallet’s whitelist. 73 | 74 | 75 | The user can send locked coins to these addresses, even if the vesting period has not yet expired. 76 | 77 | There are limitations on text comments when sending to whitelist addresses – transfers can be made either without a comment or only with comments that begin with the letters “d”, “D”, “w”, or “W”. 78 | 79 | Binary comments are also restricted. 80 | 81 | These limitations are in place in order to prevent the withdrawal of locked coins in violation of the rules. 82 | 83 | 84 | All comment restrictions for whitelist addresses are lifted after the vesting period expires. 85 | 86 | 87 | There are no comment restrictions for non-whitelist addresses. 88 | 89 | ## 7. Validation Participation 90 | 91 | Often, users who receive vesting would like to have the option to use their locked coins for validation. 92 | 93 | This can be achieved through one of the following methods: 94 | 95 | * Add the single-nominator-pool address to the whitelist. 96 | 97 | The user launches their validator with a single-nominator-pool. 98 | The `owner_address` of the pool must be the vesting wallet address. 99 | Operations like `SEND_RAW_MSG` and `UPGRADE` are not available until the vesting period expires. 100 | 101 | > Please note that the same single-nominator-pool cannot be added to the whitelists of multiple different vesting smart contracts. It can only be added to the vesting smart contract which is the owner of the pool. 102 | 103 | Here are the instructions for creating a single-nominator-pool and running it at mytonctrl - https://github.com/orbs-network/single-nominator#using-this-contract, https://telegra.ph/single-nominator-quick-how-to-09-25. 104 | 105 | * Add the Elector address to the whitelist for direct validation. 106 | In this case, the vesting wallet must be deployed in the masterchain. 107 | Adding the Config address to the whitelist is not required. 108 | 109 | * Add TON Whales pool to the whitelist. 110 | 111 | * Add the bemo.finance or https://tonstakers.com liquid pools to the whitelist. 112 | 113 | After making the deposit, it will also be necessary to add the jetton-wallet address of the user with pool tokens to the whitelist. 114 | During vesting, the user will have the option to return coins from the pool (exchange tokens back to Toncoin). 115 | The user will not have the ability to perform other operations with the pool tokens (such as sending them to others etc.) until the vesting period expires. 116 | The user will not have the ability to vote until the vesting period expires. 117 | 118 | > Please note that the possibility of bypassing the rules to withdraw locked coins through whales, bemo or tonstakers.com has not been investigated. 119 | 120 | ⚠️ Unfortunately, at the moment, nominator-pools cannot be added to the whitelist. This will be possible with a new version of the nominator-pool contracts. The user will be able to aggregate funds from multiple vesting wallets into one pool. 121 | 122 | When adding a new address to the whitelist, vesting.ton.org checks the validity of the contract at that address (its code and parameters) and displays the result on the screen. For example, if you add a single-nominator-pool address, but vesting.ton.org does not confirm that it is a single-nominator-pool, you will not be able to add that address. 123 | -------------------------------------------------------------------------------- /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": "vesting-wallet", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "npx blueprint run", 7 | "build": "npx blueprint build", 8 | "test": "jest" 9 | }, 10 | "devDependencies": { 11 | "@ton-community/blueprint": "0.12.0", 12 | "@ton-community/sandbox": "^0.11.0", 13 | "@ton-community/test-utils": "^0.2.0", 14 | "@types/jest": "^29.5.0", 15 | "@types/node": "^20.2.5", 16 | "jest": "^29.5.0", 17 | "prettier": "^2.8.6", 18 | "ton": "^13.4.1", 19 | "ton-core": "^0.49.0", 20 | "ton-crypto": "^3.2.0", 21 | "ts-jest": "^29.0.5", 22 | "ts-node": "^10.9.1", 23 | "typescript": "^4.9.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /scripts/deployVestingWallet.ts: -------------------------------------------------------------------------------- 1 | import { toNano } from 'ton-core'; 2 | import { VestingWallet } from '../wrappers/VestingWallet'; 3 | import { compile, NetworkProvider } from '@ton-community/blueprint'; 4 | 5 | export async function run(provider: NetworkProvider) { 6 | const vestingWallet = provider.open( 7 | VestingWallet.createFromConfig( 8 | { 9 | }, 10 | await compile('VestingWallet') 11 | ) 12 | ); 13 | 14 | await vestingWallet.sendDeploy(provider.sender(), toNano('0.05')); 15 | 16 | await provider.waitForDeploy(vestingWallet.address); 17 | 18 | console.log('lockupData', await vestingWallet.getVestingData()); 19 | } 20 | -------------------------------------------------------------------------------- /tests/VestingWallet.spec.ts: -------------------------------------------------------------------------------- 1 | import {Blockchain, SandboxContract, TreasuryContract} from '@ton-community/sandbox'; 2 | import {Address, beginCell, Cell, internal, MessageRelaxed, SenderArguments, SendMode, toNano} from 'ton-core'; 3 | import {ErrorCodes, Opcodes, VestingWallet} from '../wrappers/VestingWallet'; 4 | import '@ton-community/test-utils'; 5 | import {compile} from '@ton-community/blueprint'; 6 | import {createWalletTransferV3} from "ton/dist/wallets/signing/createWalletTransfer"; 7 | import {KeyPair, keyPairFromSeed} from "ton-crypto"; 8 | import {base64Decode} from "@ton-community/sandbox/dist/utils/base64"; 9 | 10 | function senderArgsToMessageRelaxed(args: SenderArguments): MessageRelaxed { 11 | return internal({ 12 | to: args.to, 13 | value: args.value, 14 | init: args.init, 15 | body: args.body, 16 | bounce: args.bounce 17 | }) 18 | } 19 | 20 | const addresses = [ 21 | '-1:0073f6ed7a84ac7d90739db7741f9d487478854b69960769f74859081e592d1c', 22 | '0:2073f6ed7a84ac7d90739db7741f9d487478854b69960769f74859081e592d1c', 23 | '-1:3073f6ed7a84ac7d90739db7741f9d487478854b69960769f74859081e592d1c', 24 | '0:4073f6ed7a84ac7d90739db7741f9d487478854b69960769f74859081e592d1c', 25 | '0:5073f6ed7a84ac7d90739db7741f9d487478854b69960769f74859081e592d1c', 26 | '0:6073f6ed7a84ac7d90739db7741f9d487478854b69960769f74859081e592d1c', 27 | '-1:7073f6ed7a84ac7d90739db7741f9d487478854b69960769f74859081e592d1c', 28 | '0:8073f6ed7a84ac7d90739db7741f9d487478854b69960769f74859081e592d1c', 29 | '0:9073f6ed7a84ac7d90739db7741f9d487478854b69960769f74859081e592d1c', 30 | '0:A073f6ed7a84ac7d90739db7741f9d487478854b69960769f74859081e590000', 31 | ].map(addressString => Address.parse(addressString)); 32 | 33 | const SUB_WALLET_ID = 345; 34 | const VESTING_START_TIME = 1689422684; 35 | const VESTING_TOTAL_DURATION = 60 * 60 * 24 * 30 * 12; 36 | const UNLOCK_PERIOD = 60 * 60 * 24 * 30; 37 | const CLIFF_DURATION = 60 * 60 * 24 * 30 * 2; 38 | const VESTING_TOTAL_AMOUNT = toNano(123000n); 39 | 40 | const ELECTOR_ADDRESS = Address.parse('Ef8zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM0vF'); 41 | const CONFIG_ADDRESS = Address.parse('Ef9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVbxn'); 42 | 43 | describe('VestingWallet', () => { 44 | let code: Cell; 45 | 46 | beforeAll(async () => { 47 | code = await compile('VestingWallet'); 48 | }); 49 | 50 | let ownerKeyPair: KeyPair; 51 | let notOwnerKeyPair: KeyPair; 52 | 53 | let blockchain: Blockchain; 54 | let vestingWallet: SandboxContract; 55 | let vestingSender: SandboxContract 56 | let owner: SandboxContract 57 | 58 | beforeEach(async () => { 59 | blockchain = await Blockchain.create(); 60 | 61 | ownerKeyPair = keyPairFromSeed(Buffer.from(base64Decode('vt58J2v6FaBuXFGcyGtqT5elpVxcZ+I1zgu/GUfA5uY='))); 62 | notOwnerKeyPair = keyPairFromSeed(Buffer.from(base64Decode('vt59J2v6FaBuXFGcyGtqT5elpVxcZ+I1zgu/GUfA5uY='))); 63 | 64 | vestingSender = await blockchain.treasury('vestingSender'); 65 | owner = await blockchain.treasury('owner'); 66 | 67 | vestingWallet = blockchain.openContract( 68 | VestingWallet.createFromConfig( 69 | { 70 | subWalletId: SUB_WALLET_ID, 71 | publicKeyHex: ownerKeyPair.publicKey.toString('hex'), 72 | vestingStartTime: VESTING_START_TIME, 73 | vestingTotalDuration: VESTING_TOTAL_DURATION, 74 | unlockPeriod: UNLOCK_PERIOD, 75 | cliffDuration: CLIFF_DURATION, 76 | vestingTotalAmount: VESTING_TOTAL_AMOUNT, 77 | vestingSenderAddress: vestingSender.address, 78 | ownerAddress: owner.address 79 | }, 80 | code 81 | ) 82 | ); 83 | 84 | const deployer = await blockchain.treasury('deployer'); 85 | const deployResult = await vestingWallet.sendDeploy(deployer.getSender(), toNano('0.05')); 86 | 87 | expect(deployResult.transactions).toHaveTransaction({ 88 | from: deployer.address, 89 | to: vestingWallet.address, 90 | deploy: true, 91 | success: true, 92 | }); 93 | 94 | const topUpResult = await vestingWallet.sendSimple(owner.getSender(), { 95 | value: toNano('123000'), 96 | }); 97 | 98 | expect(topUpResult.transactions).toHaveTransaction({ 99 | from: owner.address, 100 | to: vestingWallet.address, 101 | success: true 102 | }); 103 | expect(topUpResult.transactions.length).toBe(2); 104 | 105 | await checkLockupData(); 106 | 107 | }); 108 | 109 | async function checkLockupData(expectedSeqno?: number) { 110 | const seqno = await vestingWallet.getSeqno(); 111 | expect(seqno).toBe(expectedSeqno || 0); 112 | 113 | const subWalletId = await vestingWallet.getSubWalletId(); 114 | expect(subWalletId).toBe(SUB_WALLET_ID); 115 | 116 | const publicKeyHex = await vestingWallet.getPublicKeyHex(); 117 | expect(publicKeyHex).toBe(ownerKeyPair.publicKey.toString('hex')); 118 | 119 | const lockupData = await vestingWallet.getVestingData(); 120 | expect(lockupData.vestingStartTime).toBe(VESTING_START_TIME); 121 | expect(lockupData.vestingTotalDuration).toBe(VESTING_TOTAL_DURATION); 122 | expect(lockupData.unlockPeriod).toBe(UNLOCK_PERIOD); 123 | expect(lockupData.cliffDuration).toBe(CLIFF_DURATION); 124 | expect(lockupData.vestingTotalAmount).toBe(VESTING_TOTAL_AMOUNT); 125 | expect(lockupData.vestingSenderAddress.toString()).toBe(vestingSender.address.toString()); 126 | expect(lockupData.ownerAddress.toString()).toBe(owner.address.toString()); 127 | } 128 | 129 | it('should deploy', async () => { 130 | // the check is done inside beforeEach 131 | // blockchain and vestingWallet are ready to use 132 | 133 | await checkLockupData(); 134 | 135 | }); 136 | 137 | // ADD WHITELIST 138 | 139 | async function addWhitelist(count: number, increaser: SandboxContract) { 140 | const whitelistBefore = await vestingWallet.getWhitelist(); 141 | expect(whitelistBefore.length).toBe(0); 142 | 143 | const result = await vestingWallet.sendAddWhitelist(increaser.getSender(), { 144 | queryId: 123 + count, 145 | value: toNano('1'), 146 | addresses: addresses.slice(0, count) 147 | }); 148 | 149 | expect(result.transactions).toHaveTransaction({ 150 | from: increaser.address, 151 | to: vestingWallet.address, 152 | success: true, 153 | }); 154 | 155 | expect(result.transactions).toHaveTransaction({ 156 | from: vestingWallet.address, 157 | to: increaser.address, 158 | success: true, 159 | body: beginCell().storeUint(Opcodes.add_whitelist_response, 32).storeUint(123 + count, 64).endCell() 160 | }); 161 | 162 | expect(result.transactions.length).toBe(3); 163 | 164 | const lastTx: any = result.transactions[result.transactions.length - 1]; 165 | expect(lastTx.inMessage.info.src.toString()).toBe(vestingWallet.address.toString()); 166 | expect(lastTx.inMessage.info.dest.toString()).toBe(increaser.address.toString()); 167 | expect(lastTx.inMessage.info.value.coins).toBeLessThan(toNano('1')); 168 | 169 | const whitelist = await vestingWallet.getWhitelist(); 170 | const whitelistStrings = whitelist.map((address: Address) => address.toString()); 171 | expect(whitelist.length).toBe(count); 172 | 173 | for (let i = 0; i < count; i++) { 174 | expect(whitelistStrings.indexOf(addresses[i].toString()) > -1).toBeTruthy(); 175 | expect(await vestingWallet.getIsWhitelisted(addresses[i])).toBeTruthy(); 176 | } 177 | 178 | for (let i = count; i < 10; i++) { 179 | expect(await vestingWallet.getIsWhitelisted(addresses[i])).toBeFalsy(); 180 | } 181 | 182 | expect(await vestingWallet.getIsWhitelisted(Address.parse('0:0073f6ed7a84ac7d90739db7741f9d487478854b69960769f74859081e592d1c'))).toBeFalsy() 183 | 184 | await checkLockupData(); 185 | } 186 | 187 | it('add 1 whitelist', async () => { 188 | await addWhitelist(1, vestingSender); 189 | }); 190 | it('add 2 whitelist', async () => { 191 | await addWhitelist(2, vestingSender); 192 | }); 193 | it('add 3 whitelist', async () => { 194 | await addWhitelist(3, vestingSender); 195 | }); 196 | it('add 4 whitelist', async () => { 197 | await addWhitelist(4, vestingSender); 198 | }); 199 | it('add 5 whitelist', async () => { 200 | await addWhitelist(5, vestingSender); 201 | }); 202 | it('add 6 whitelist', async () => { 203 | await addWhitelist(6, vestingSender); 204 | }); 205 | it('add 7 whitelist', async () => { 206 | await addWhitelist(7, vestingSender); 207 | }); 208 | it('add 8 whitelist', async () => { 209 | await addWhitelist(8, vestingSender); 210 | }); 211 | it('add 9 whitelist', async () => { 212 | await addWhitelist(9, vestingSender); 213 | }); 214 | it('add 10 whitelist', async () => { 215 | await addWhitelist(10, vestingSender); 216 | }); 217 | 218 | async function whitelistNotVestingSender(notVestingSender: SandboxContract) { 219 | 220 | const whitelistBefore = await vestingWallet.getWhitelist(); 221 | expect(whitelistBefore.length).toBe(0); 222 | 223 | const result = await vestingWallet.sendAddWhitelist(notVestingSender.getSender(), { 224 | queryId: 123, 225 | value: toNano('1'), 226 | addresses: addresses.slice(0, 1) 227 | }); 228 | 229 | expect(result.transactions).toHaveTransaction({ 230 | from: notVestingSender.address, 231 | to: vestingWallet.address, 232 | success: true, 233 | }); 234 | expect(result.transactions.length).toBe(2); 235 | 236 | const whitelist = await vestingWallet.getWhitelist(); 237 | expect(whitelist.length).toBe(0); 238 | 239 | await checkLockupData(); 240 | } 241 | 242 | it('add whitelist not sender', async () => { 243 | const notVestingSender = await blockchain.treasury('notVestingSender'); 244 | await whitelistNotVestingSender(notVestingSender); 245 | }); 246 | 247 | it('add whitelist by owner', async () => { 248 | await whitelistNotVestingSender(owner); 249 | }); 250 | 251 | it('add whitelist twice', async () => { 252 | const increaser = vestingSender; 253 | 254 | const whitelistBefore = await vestingWallet.getWhitelist(); 255 | expect(whitelistBefore.length).toBe(0); 256 | 257 | const result = await vestingWallet.sendAddWhitelist(increaser.getSender(), { 258 | queryId: 123, 259 | value: toNano('1'), 260 | addresses: addresses.slice(0, 2) 261 | }); 262 | 263 | expect(result.transactions).toHaveTransaction({ 264 | from: increaser.address, 265 | to: vestingWallet.address, 266 | success: true, 267 | }); 268 | 269 | expect(result.transactions).toHaveTransaction({ 270 | from: vestingWallet.address, 271 | to: increaser.address, 272 | success: true, 273 | body: beginCell().storeUint(Opcodes.add_whitelist_response, 32).storeUint(123, 64).endCell() 274 | }); 275 | expect(result.transactions.length).toBe(3); 276 | 277 | const whitelist = await vestingWallet.getWhitelist(); 278 | const whitelistStrings = whitelist.map((address: Address) => address.toString()); 279 | expect(whitelist.length).toBe(2); 280 | 281 | expect(whitelistStrings.indexOf(addresses[0].toString()) > -1).toBeTruthy(); 282 | expect(whitelistStrings.indexOf(addresses[1].toString()) > -1).toBeTruthy(); 283 | expect(whitelistStrings.indexOf(addresses[4].toString()) > -1).toBeFalsy(); 284 | expect(await vestingWallet.getIsWhitelisted(addresses[0])).toBeTruthy(); 285 | expect(await vestingWallet.getIsWhitelisted(addresses[1])).toBeTruthy(); 286 | expect(await vestingWallet.getIsWhitelisted(addresses[4])).toBeFalsy(); 287 | 288 | const result2 = await vestingWallet.sendAddWhitelist(increaser.getSender(), { 289 | queryId: 777, 290 | value: toNano('1'), 291 | addresses: [addresses[4]] 292 | }); 293 | 294 | expect(result2.transactions).toHaveTransaction({ 295 | from: increaser.address, 296 | to: vestingWallet.address, 297 | success: true, 298 | }); 299 | 300 | expect(result2.transactions).toHaveTransaction({ 301 | from: vestingWallet.address, 302 | to: increaser.address, 303 | success: true, 304 | body: beginCell().storeUint(Opcodes.add_whitelist_response, 32).storeUint(777, 64).endCell() 305 | }); 306 | expect(result2.transactions.length).toBe(3); 307 | 308 | const lastTx: any = result2.transactions[result2.transactions.length - 1]; 309 | expect(lastTx.inMessage.info.src.toString()).toBe(vestingWallet.address.toString()); 310 | expect(lastTx.inMessage.info.dest.toString()).toBe(increaser.address.toString()); 311 | expect(lastTx.inMessage.info.value.coins).toBeLessThan(toNano('1')); 312 | 313 | const whitelist2 = await vestingWallet.getWhitelist(); 314 | const whitelistStrings2 = whitelist2.map((address: Address) => address.toString()); 315 | expect(whitelist2.length).toBe(3); 316 | 317 | expect(whitelistStrings2.indexOf(addresses[0].toString()) > -1).toBeTruthy(); 318 | expect(whitelistStrings2.indexOf(addresses[1].toString()) > -1).toBeTruthy(); 319 | expect(whitelistStrings2.indexOf(addresses[4].toString()) > -1).toBeTruthy(); 320 | expect(await vestingWallet.getIsWhitelisted(addresses[0])).toBeTruthy(); 321 | expect(await vestingWallet.getIsWhitelisted(addresses[1])).toBeTruthy(); 322 | expect(await vestingWallet.getIsWhitelisted(addresses[4])).toBeTruthy(); 323 | 324 | await checkLockupData(); 325 | }); 326 | 327 | // TOPUP 328 | 329 | async function topUp(sender: SandboxContract) { 330 | const whitelistBefore = await vestingWallet.getWhitelist(); 331 | expect(whitelistBefore.length).toBe(0); 332 | 333 | // empty message 334 | 335 | const result = await vestingWallet.sendSimple(sender.getSender(), { 336 | value: toNano('1') 337 | }); 338 | 339 | expect(result.transactions).toHaveTransaction({ 340 | from: sender.address, 341 | to: vestingWallet.address, 342 | success: true, 343 | }); 344 | expect(result.transactions.length).toBe(2); 345 | 346 | // text comment 347 | 348 | const result2 = await vestingWallet.sendSimple(sender.getSender(), { 349 | value: toNano('1'), 350 | comment: 'giftgiftgiftgiftgiftgift' 351 | }); 352 | 353 | expect(result2.transactions).toHaveTransaction({ 354 | from: sender.address, 355 | to: vestingWallet.address, 356 | success: true, 357 | }); 358 | expect(result2.transactions.length).toBe(2); 359 | 360 | // empty text comment 361 | 362 | const result3 = await vestingWallet.sendOp(sender.getSender(), { 363 | value: toNano('1'), 364 | op: 0 365 | }); 366 | 367 | expect(result3.transactions).toHaveTransaction({ 368 | from: sender.address, 369 | to: vestingWallet.address, 370 | success: true, 371 | }); 372 | expect(result3.transactions.length).toBe(2); 373 | 374 | // any OP 375 | 376 | const result4 = await vestingWallet.sendOp(sender.getSender(), { 377 | value: toNano('1'), 378 | op: 0xd6745240 379 | }); 380 | 381 | expect(result4.transactions).toHaveTransaction({ 382 | from: sender.address, 383 | to: vestingWallet.address, 384 | success: true, 385 | }); 386 | expect(result4.transactions.length).toBe(2); 387 | 388 | // 389 | 390 | const whitelist = await vestingWallet.getWhitelist(); 391 | expect(whitelist.length).toBe(0); 392 | 393 | await checkLockupData(); 394 | } 395 | 396 | 397 | it('sender can topup', async () => { 398 | await topUp(vestingSender); 399 | }); 400 | 401 | 402 | it('anyone can topup', async () => { 403 | const notVestingSender = await blockchain.treasury('notVestingSender'); 404 | await topUp(notVestingSender); 405 | }); 406 | 407 | it('owner can topup', async () => { 408 | await topUp(owner); 409 | }); 410 | 411 | // locked amount 412 | 413 | it('get_unlocked_amount', async () => { 414 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME - 100)).toBe(toNano(123000n)); 415 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME)).toBe(toNano(123000n)); 416 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD)).toBe(toNano(123000n)); 417 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + CLIFF_DURATION - 1)).toBe(toNano(123000n)); 418 | 419 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + CLIFF_DURATION)).toBe(toNano(123000n - 123000n * 2n / 12n)); 420 | 421 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 3 - 1)).toBe(toNano(123000n - 123000n * 2n / 12n)); 422 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 3)).toBe(toNano(123000n - 123000n * 3n / 12n)); 423 | 424 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 4 - 1)).toBe(toNano(123000n - 123000n * 3n / 12n)); 425 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 4)).toBe(toNano(123000n - 123000n * 4n / 12n)); 426 | 427 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 7 - 1)).toBe(toNano(123000n - 123000n * 6n / 12n)); 428 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 7)).toBe(toNano(123000n - 123000n * 7n / 12n)); 429 | 430 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 9 - 1)).toBe(toNano(123000n - 123000n * 8n / 12n)); 431 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 9)).toBe(toNano(123000n - 123000n * 9n / 12n)); 432 | 433 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 10 - 1)).toBe(toNano(123000n - 123000n * 9n / 12n)); 434 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 10)).toBe(toNano(123000n - 123000n * 10n / 12n)); 435 | 436 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 11 - 1)).toBe(toNano(123000n - 123000n * 10n / 12n)); 437 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 11)).toBe(toNano(123000n - 123000n * 11n / 12n)); 438 | 439 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 12 - 1)).toBe(toNano(123000n - 123000n * 11n / 12n)); 440 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 12)).toBe(toNano(0n)); 441 | 442 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + VESTING_TOTAL_DURATION)).toBe(toNano(0n)); 443 | }); 444 | 445 | it('get_unlocked_amount no cliff', async () => { 446 | const vestingWallet = blockchain.openContract( 447 | VestingWallet.createFromConfig( 448 | { 449 | subWalletId: SUB_WALLET_ID, 450 | publicKeyHex: ownerKeyPair.publicKey.toString('hex'), 451 | vestingStartTime: VESTING_START_TIME, 452 | vestingTotalDuration: VESTING_TOTAL_DURATION, 453 | unlockPeriod: UNLOCK_PERIOD, 454 | cliffDuration: 0, 455 | vestingTotalAmount: VESTING_TOTAL_AMOUNT, 456 | vestingSenderAddress: vestingSender.address, 457 | ownerAddress: owner.address 458 | }, 459 | code 460 | ) 461 | ); 462 | 463 | const deployer = await blockchain.treasury('deployer'); 464 | const deployResult = await vestingWallet.sendDeploy(deployer.getSender(), toNano('0.05')); 465 | 466 | expect(deployResult.transactions).toHaveTransaction({ 467 | from: deployer.address, 468 | to: vestingWallet.address, 469 | deploy: true, 470 | success: true, 471 | }); 472 | 473 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME - 100)).toBe(toNano(123000n)); 474 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME)).toBe(toNano(123000n)); 475 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD)).toBe(toNano(123000n - 123000n * 1n / 12n)); 476 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + CLIFF_DURATION - 1)).toBe(toNano(123000n - 123000n * 1n / 12n)); 477 | 478 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + CLIFF_DURATION)).toBe(toNano(123000n - 123000n * 2n / 12n)); 479 | 480 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 3 - 1)).toBe(toNano(123000n - 123000n * 2n / 12n)); 481 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 3)).toBe(toNano(123000n - 123000n * 3n / 12n)); 482 | 483 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 4 - 1)).toBe(toNano(123000n - 123000n * 3n / 12n)); 484 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 4)).toBe(toNano(123000n - 123000n * 4n / 12n)); 485 | 486 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 7 - 1)).toBe(toNano(123000n - 123000n * 6n / 12n)); 487 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 7)).toBe(toNano(123000n - 123000n * 7n / 12n)); 488 | 489 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 9 - 1)).toBe(toNano(123000n - 123000n * 8n / 12n)); 490 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 9)).toBe(toNano(123000n - 123000n * 9n / 12n)); 491 | 492 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 10 - 1)).toBe(toNano(123000n - 123000n * 9n / 12n)); 493 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 10)).toBe(toNano(123000n - 123000n * 10n / 12n)); 494 | 495 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 11 - 1)).toBe(toNano(123000n - 123000n * 10n / 12n)); 496 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 11)).toBe(toNano(123000n - 123000n * 11n / 12n)); 497 | 498 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 12 - 1)).toBe(toNano(123000n - 123000n * 11n / 12n)); 499 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 12)).toBe(toNano(0n)); 500 | 501 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + VESTING_TOTAL_DURATION)).toBe(toNano(0n)); 502 | }); 503 | 504 | it('get_unlocked_amount unlock = duration', async () => { 505 | const vestingWallet = blockchain.openContract( 506 | VestingWallet.createFromConfig( 507 | { 508 | subWalletId: SUB_WALLET_ID, 509 | publicKeyHex: ownerKeyPair.publicKey.toString('hex'), 510 | vestingStartTime: VESTING_START_TIME, 511 | vestingTotalDuration: VESTING_TOTAL_DURATION, 512 | unlockPeriod: VESTING_TOTAL_DURATION, 513 | cliffDuration: 0, 514 | vestingTotalAmount: VESTING_TOTAL_AMOUNT, 515 | vestingSenderAddress: vestingSender.address, 516 | ownerAddress: owner.address 517 | }, 518 | code 519 | ) 520 | ); 521 | 522 | const deployer = await blockchain.treasury('deployer'); 523 | const deployResult = await vestingWallet.sendDeploy(deployer.getSender(), toNano('0.05')); 524 | 525 | expect(deployResult.transactions).toHaveTransaction({ 526 | from: deployer.address, 527 | to: vestingWallet.address, 528 | deploy: true, 529 | success: true, 530 | }); 531 | 532 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME - 100)).toBe(toNano(123000n)); 533 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME)).toBe(toNano(123000n)); 534 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + VESTING_TOTAL_DURATION / 2)).toBe(toNano(123000n)); 535 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + VESTING_TOTAL_DURATION - 1)).toBe(toNano(123000n)); 536 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + VESTING_TOTAL_DURATION)).toBe(toNano(0n)); 537 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + VESTING_TOTAL_DURATION + 1)).toBe(toNano(0n)); 538 | }); 539 | 540 | 541 | it('get_unlocked_amount total_duration == unlock_period', async () => { 542 | const vestingWallet = blockchain.openContract( 543 | VestingWallet.createFromConfig( 544 | { 545 | subWalletId: SUB_WALLET_ID, 546 | publicKeyHex: ownerKeyPair.publicKey.toString('hex'), 547 | vestingStartTime: VESTING_START_TIME, 548 | vestingTotalDuration: VESTING_TOTAL_DURATION, 549 | unlockPeriod: VESTING_TOTAL_DURATION, 550 | cliffDuration: 0, 551 | vestingTotalAmount: VESTING_TOTAL_AMOUNT, 552 | vestingSenderAddress: vestingSender.address, 553 | ownerAddress: owner.address 554 | }, 555 | code 556 | ) 557 | ); 558 | 559 | const deployer = await blockchain.treasury('deployer'); 560 | const deployResult = await vestingWallet.sendDeploy(deployer.getSender(), toNano('0.05')); 561 | 562 | expect(deployResult.transactions).toHaveTransaction({ 563 | from: deployer.address, 564 | to: vestingWallet.address, 565 | deploy: true, 566 | success: true, 567 | }); 568 | 569 | 570 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME - 100)).toBe(toNano(123000n)); 571 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME)).toBe(toNano(123000n)); 572 | for (let i = 0; i <= 11; i++) { 573 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * i)).toBe(toNano(123000n)); 574 | } 575 | 576 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + UNLOCK_PERIOD * 12)).toBe(toNano(0n)); 577 | 578 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + VESTING_TOTAL_DURATION)).toBe(toNano(0n)); 579 | expect(await vestingWallet.getLockedAmount(VESTING_START_TIME + VESTING_TOTAL_DURATION * 2)).toBe(toNano(0n)); 580 | }); 581 | 582 | async function transferSuccess(time: number, value: bigint) { 583 | blockchain.now = time; 584 | 585 | const notVestingSender = await blockchain.treasury('notVestingSender'); 586 | 587 | const whitelistBefore = await vestingWallet.getWhitelist(); 588 | expect(whitelistBefore.length).toBe(0); 589 | 590 | const t = createWalletTransferV3({ 591 | seqno: 0, 592 | sendMode: 0, 593 | walletId: 0, 594 | messages: [ 595 | senderArgsToMessageRelaxed({ 596 | to: notVestingSender.address, 597 | value: value, 598 | bounce: true 599 | }) 600 | ], 601 | secretKey: Buffer.from(new Uint8Array(64)) 602 | }); 603 | 604 | const result = await vestingWallet.sendInternalTransfer(owner.getSender(), { 605 | value: toNano('1'), 606 | queryId: 567, 607 | sendMode: 3, 608 | msg: t.beginParse().loadRef() 609 | }); 610 | 611 | expect(result.transactions).toHaveTransaction({ 612 | from: owner.address, 613 | to: vestingWallet.address, 614 | success: true 615 | }); 616 | 617 | expect(result.transactions).toHaveTransaction({ 618 | from: vestingWallet.address, 619 | to: owner.address, 620 | body: beginCell().storeUint(Opcodes.send_response, 32).storeUint(567, 64).endCell() 621 | }); 622 | 623 | expect(result.transactions).toHaveTransaction({ 624 | from: vestingWallet.address, 625 | to: notVestingSender.address, 626 | value: value, 627 | }); 628 | expect(result.transactions.length).toBe(4); 629 | 630 | const lastTx: any = result.transactions[result.transactions.length - 1]; 631 | expect(lastTx.inMessage.info.src.toString()).toBe(vestingWallet.address.toString()); 632 | expect(lastTx.inMessage.info.dest.toString()).toBe(owner.address.toString()); 633 | expect(lastTx.inMessage.info.value.coins).toBeLessThan(toNano('1')); 634 | 635 | const whitelist = await vestingWallet.getWhitelist(); 636 | expect(whitelist.length).toBe(0); 637 | 638 | await checkLockupData(); 639 | } 640 | 641 | async function transferFail(time: number, value: bigint) { 642 | blockchain.now = time; 643 | 644 | const notVestingSender = await blockchain.treasury('notVestingSender'); 645 | 646 | const whitelistBefore = await vestingWallet.getWhitelist(); 647 | expect(whitelistBefore.length).toBe(0); 648 | 649 | const t = createWalletTransferV3({ 650 | seqno: 0, 651 | sendMode: SendMode.IGNORE_ERRORS + SendMode.PAY_GAS_SEPARATELY, 652 | walletId: 0, 653 | messages: [ 654 | senderArgsToMessageRelaxed({ 655 | to: notVestingSender.address, 656 | value: value, 657 | bounce: true 658 | }) 659 | ], 660 | secretKey: Buffer.from(new Uint8Array(64)) 661 | }); 662 | 663 | const result = await vestingWallet.sendInternalTransfer(owner.getSender(), { 664 | value: toNano('1'), 665 | queryId: 567, 666 | sendMode: 3, 667 | msg: t.beginParse().loadRef() 668 | }); 669 | 670 | expect(result.transactions).toHaveTransaction({ 671 | from: owner.address, 672 | to: vestingWallet.address, 673 | success: true 674 | }); 675 | 676 | expect(result.transactions.length).toBe(3); 677 | 678 | const lastTx: any = result.transactions[result.transactions.length - 1]; 679 | expect(lastTx.inMessage.info.src.toString()).toBe(vestingWallet.address.toString()); 680 | expect(lastTx.inMessage.info.dest.toString()).toBe(owner.address.toString()); 681 | expect(lastTx.inMessage.info.value.coins).toBeLessThan(toNano('1')); 682 | 683 | const whitelist = await vestingWallet.getWhitelist(); 684 | expect(whitelist.length).toBe(0); 685 | 686 | await checkLockupData(); 687 | } 688 | 689 | it('owner cant internal transfer', async () => { 690 | await transferFail( 691 | VESTING_START_TIME + UNLOCK_PERIOD, 692 | toNano(123000n * 1n / 12n) 693 | ) 694 | }); 695 | 696 | it('owner can internal 2/12 transfer', async () => { 697 | await transferSuccess( 698 | VESTING_START_TIME + CLIFF_DURATION, 699 | toNano(123000n * 2n / 12n) 700 | ) 701 | }); 702 | 703 | it('owner cant internal 4/12 transfer', async () => { 704 | await transferFail( 705 | VESTING_START_TIME + UNLOCK_PERIOD * 4 - 1, 706 | toNano(123000n * 4n / 12n) 707 | ) 708 | }); 709 | 710 | it('owner can internal 4/12 transfer', async () => { 711 | await transferSuccess( 712 | VESTING_START_TIME + UNLOCK_PERIOD * 4, 713 | toNano(123000n * 4n / 12n) 714 | ) 715 | }); 716 | 717 | it('owner can internal 12/12 transfer', async () => { 718 | await transferSuccess( 719 | VESTING_START_TIME + VESTING_TOTAL_DURATION + 1, 720 | toNano(123000n) 721 | ) 722 | }); 723 | 724 | // INTERNAL SEND 725 | 726 | it('owner can internal transfer', async () => { 727 | blockchain.now = VESTING_START_TIME + VESTING_TOTAL_DURATION + 1; 728 | 729 | const whitelistBefore = await vestingWallet.getWhitelist(); 730 | expect(whitelistBefore.length).toBe(0); 731 | 732 | const t = createWalletTransferV3({ 733 | seqno: 0, 734 | sendMode: SendMode.IGNORE_ERRORS + SendMode.PAY_GAS_SEPARATELY, 735 | walletId: 0, 736 | messages: [ 737 | senderArgsToMessageRelaxed({ 738 | to: vestingSender.address, 739 | value: toNano('122999.9'), 740 | bounce: true 741 | }) 742 | ], 743 | secretKey: Buffer.from(new Uint8Array(64)) 744 | }); 745 | 746 | const result = await vestingWallet.sendInternalTransfer(owner.getSender(), { 747 | value: toNano('1'), 748 | queryId: 567, 749 | sendMode: 3, 750 | msg: t.beginParse().loadRef() 751 | }); 752 | 753 | expect(result.transactions).toHaveTransaction({ 754 | from: owner.address, 755 | to: vestingWallet.address, 756 | success: true 757 | }); 758 | 759 | expect(result.transactions).toHaveTransaction({ 760 | from: vestingWallet.address, 761 | to: owner.address, 762 | body: beginCell().storeUint(Opcodes.send_response, 32).storeUint(567, 64).endCell() 763 | }); 764 | 765 | expect(result.transactions).toHaveTransaction({ 766 | from: vestingWallet.address, 767 | to: vestingSender.address, 768 | value: toNano('122999.9'), 769 | }); 770 | expect(result.transactions.length).toBe(4); 771 | 772 | const lastTx: any = result.transactions[result.transactions.length - 1]; 773 | expect(lastTx.inMessage.info.src.toString()).toBe(vestingWallet.address.toString()); 774 | expect(lastTx.inMessage.info.dest.toString()).toBe(owner.address.toString()); 775 | expect(lastTx.inMessage.info.value.coins).toBeLessThan(toNano('1')); 776 | 777 | const whitelist = await vestingWallet.getWhitelist(); 778 | expect(whitelist.length).toBe(0); 779 | 780 | await checkLockupData(); 781 | }); 782 | 783 | it('no restriction after vesting', async () => { 784 | blockchain.now = VESTING_START_TIME + VESTING_TOTAL_DURATION + 1; 785 | 786 | const notVestingSender = await blockchain.treasury('notVestingSender'); 787 | 788 | const whitelistBefore = await vestingWallet.getWhitelist(); 789 | expect(whitelistBefore.length).toBe(0); 790 | 791 | const t = createWalletTransferV3({ 792 | seqno: 0, 793 | sendMode: SendMode.PAY_GAS_SEPARATELY, 794 | walletId: 0, 795 | messages: [ 796 | senderArgsToMessageRelaxed({ 797 | to: notVestingSender.address, 798 | value: toNano('122999.9'), 799 | bounce: false, 800 | body: beginCell().storeUint(0, 32).storeStringTail("y").endCell() 801 | }) 802 | ], 803 | secretKey: Buffer.from(new Uint8Array(64)) 804 | }); 805 | 806 | const result = await vestingWallet.sendInternalTransfer(owner.getSender(), { 807 | value: toNano('1'), 808 | queryId: 567, 809 | sendMode: 3, 810 | msg: t.beginParse().loadRef() 811 | }); 812 | 813 | expect(result.transactions).toHaveTransaction({ 814 | from: owner.address, 815 | to: vestingWallet.address, 816 | success: true 817 | }); 818 | 819 | expect(result.transactions).toHaveTransaction({ 820 | from: vestingWallet.address, 821 | to: owner.address, 822 | body: beginCell().storeUint(Opcodes.send_response, 32).storeUint(567, 64).endCell() 823 | }); 824 | 825 | expect(result.transactions).toHaveTransaction({ 826 | from: vestingWallet.address, 827 | to: notVestingSender.address, 828 | value: toNano('122999.9'), 829 | }); 830 | expect(result.transactions.length).toBe(4); 831 | 832 | const lastTx: any = result.transactions[result.transactions.length - 1]; 833 | expect(lastTx.inMessage.info.src.toString()).toBe(vestingWallet.address.toString()); 834 | expect(lastTx.inMessage.info.dest.toString()).toBe(owner.address.toString()); 835 | expect(lastTx.inMessage.info.value.coins).toBeLessThan(toNano('1')); 836 | 837 | const whitelist = await vestingWallet.getWhitelist(); 838 | expect(whitelist.length).toBe(0); 839 | 840 | await checkLockupData(); 841 | }); 842 | 843 | async function transferNotOwner(sender: SandboxContract) { 844 | blockchain.now = VESTING_START_TIME + VESTING_TOTAL_DURATION + 1; 845 | 846 | const whitelistBefore = await vestingWallet.getWhitelist(); 847 | expect(whitelistBefore.length).toBe(0); 848 | 849 | const t = createWalletTransferV3({ 850 | seqno: 0, 851 | sendMode: SendMode.IGNORE_ERRORS + SendMode.PAY_GAS_SEPARATELY, 852 | walletId: 0, 853 | messages: [ 854 | senderArgsToMessageRelaxed({ 855 | to: vestingSender.address, 856 | value: 9n, 857 | bounce: true 858 | }) 859 | ], 860 | secretKey: Buffer.from(new Uint8Array(64)) 861 | }); 862 | 863 | const result = await vestingWallet.sendInternalTransfer(sender.getSender(), { 864 | value: toNano('1'), 865 | queryId: 567, 866 | sendMode: 3, 867 | msg: t.beginParse().loadRef() 868 | }); 869 | 870 | expect(result.transactions).toHaveTransaction({ 871 | from: sender.address, 872 | to: vestingWallet.address, 873 | success: true 874 | }); 875 | expect(result.transactions.length).toBe(2); 876 | 877 | const whitelist = await vestingWallet.getWhitelist(); 878 | expect(whitelist.length).toBe(0); 879 | 880 | await checkLockupData(); 881 | } 882 | 883 | it('not owner can not do internal transfer', async () => { 884 | const notVestingSender = await blockchain.treasury('notVestingSender'); 885 | await transferNotOwner(notVestingSender); 886 | }); 887 | 888 | it('vesting sender can not do internal transfer', async () => { 889 | await transferNotOwner(vestingSender); 890 | }); 891 | 892 | // restrictions 893 | 894 | async function transferReject(exitCode: number, sender: SandboxContract, to: Address, isWhitelist: boolean, sendMode: number, bounceable: boolean, hasStateInit: boolean, comment: String | number | undefined) { 895 | blockchain.now = VESTING_START_TIME + UNLOCK_PERIOD * 7; 896 | 897 | const whitelistBefore = await vestingWallet.getWhitelist(); 898 | expect(whitelistBefore.length).toBe(0); 899 | 900 | if (isWhitelist) { 901 | await vestingWallet.sendAddWhitelist(vestingSender.getSender(), { 902 | queryId: 111, 903 | value: toNano('1'), 904 | addresses: [to] 905 | }); 906 | 907 | expect(await vestingWallet.getIsWhitelisted(to)).toBeTruthy(); 908 | } 909 | 910 | const stateInit = hasStateInit ? { 911 | code, data: beginCell().endCell() 912 | } : undefined; 913 | 914 | let body: Cell | undefined = undefined; 915 | if (comment) { 916 | body = (typeof comment === 'string') ? 917 | beginCell().storeUint(0, 32).storeStringTail(comment as string).endCell() : 918 | beginCell().storeUint(comment as number, 32).storeUint(0x456, 64).endCell(); 919 | } 920 | 921 | const t = createWalletTransferV3({ 922 | seqno: 0, 923 | sendMode: sendMode, 924 | walletId: 0, 925 | messages: [ 926 | senderArgsToMessageRelaxed({ 927 | to: to, 928 | value: toNano('3'), 929 | bounce: bounceable, 930 | body: body, 931 | init: stateInit 932 | }) 933 | ], 934 | secretKey: Buffer.from(new Uint8Array(64)) 935 | }); 936 | 937 | const result = await vestingWallet.sendInternalTransfer(sender.getSender(), { 938 | value: toNano('1'), 939 | queryId: 567, 940 | sendMode: sendMode, 941 | msg: t.beginParse().loadRef() 942 | }); 943 | 944 | expect(result.transactions).toHaveTransaction({ 945 | from: sender.address, 946 | to: vestingWallet.address, 947 | success: false, 948 | exitCode: exitCode 949 | }); 950 | expect(result.transactions.length).toBe(3); 951 | 952 | const whitelist = await vestingWallet.getWhitelist(); 953 | expect(whitelist.length).toBe(isWhitelist ? 1 : 0); 954 | 955 | if (isWhitelist) { 956 | expect(await vestingWallet.getIsWhitelisted(to)).toBeTruthy(); 957 | } 958 | 959 | await checkLockupData(); 960 | } 961 | 962 | async function transferAllow(time: number, sender: SandboxContract, to: Address, isWhitelist: boolean, sendMode: number, bounceable: boolean, hasStateInit: boolean, comment: String | number | undefined) { 963 | blockchain.now = time; 964 | 965 | const whitelistBefore = await vestingWallet.getWhitelist(); 966 | expect(whitelistBefore.length).toBe(0); 967 | 968 | if (isWhitelist) { 969 | await vestingWallet.sendAddWhitelist(vestingSender.getSender(), { 970 | queryId: 111, 971 | value: toNano('1'), 972 | addresses: [to] 973 | }); 974 | 975 | expect(await vestingWallet.getIsWhitelisted(to)).toBeTruthy(); 976 | } 977 | 978 | const stateInit = hasStateInit ? { 979 | code, data: beginCell().endCell() 980 | } : undefined; 981 | 982 | let body: Cell | undefined = undefined; 983 | if (comment) { 984 | body = (typeof comment === 'string') ? 985 | beginCell().storeUint(0, 32).storeStringTail(comment as string).endCell() : 986 | beginCell().storeUint(comment as number, 32).storeUint(0x456, 64).endCell(); 987 | } 988 | 989 | const t = createWalletTransferV3({ 990 | seqno: 0, 991 | sendMode: sendMode, 992 | walletId: 0, 993 | messages: [ 994 | senderArgsToMessageRelaxed({ 995 | to: to, 996 | value: toNano('3'), 997 | bounce: bounceable, 998 | body: body, 999 | init: stateInit 1000 | }) 1001 | ], 1002 | secretKey: Buffer.from(new Uint8Array(64)) 1003 | }); 1004 | 1005 | const result = await vestingWallet.sendInternalTransfer(sender.getSender(), { 1006 | value: toNano('1'), 1007 | queryId: 567, 1008 | sendMode: sendMode, 1009 | msg: t.beginParse().loadRef() 1010 | }); 1011 | 1012 | expect(result.transactions).toHaveTransaction({ 1013 | from: owner.address, 1014 | to: vestingWallet.address, 1015 | success: true 1016 | }); 1017 | 1018 | expect(result.transactions).toHaveTransaction({ 1019 | from: vestingWallet.address, 1020 | to: owner.address, 1021 | body: beginCell().storeUint(Opcodes.send_response, 32).storeUint(567, 64).endCell() 1022 | }); 1023 | 1024 | expect(result.transactions).toHaveTransaction({ 1025 | from: vestingWallet.address, 1026 | to: to, 1027 | value: toNano('3'), 1028 | }); 1029 | expect(result.transactions.length).toBe(4); 1030 | 1031 | const lastTx: any = result.transactions[result.transactions.length - 1]; 1032 | expect(lastTx.inMessage.info.src.toString()).toBe(vestingWallet.address.toString()); 1033 | expect(lastTx.inMessage.info.dest.toString()).toBe(owner.address.toString()); 1034 | expect(lastTx.inMessage.info.value.coins).toBeLessThan(toNano('1')); 1035 | 1036 | 1037 | const whitelist = await vestingWallet.getWhitelist(); 1038 | expect(whitelist.length).toBe(isWhitelist ? 1 : 0); 1039 | 1040 | if (isWhitelist) { 1041 | expect(await vestingWallet.getIsWhitelisted(to)).toBeTruthy(); 1042 | } 1043 | 1044 | await checkLockupData(); 1045 | } 1046 | 1047 | async function transferAllowElector(time: number, sender: SandboxContract, to: Address, isWhitelist: boolean, sendMode: number, bounceable: boolean, hasStateInit: boolean, comment: String | number | undefined) { 1048 | blockchain.now = time; 1049 | 1050 | const whitelistBefore = await vestingWallet.getWhitelist(); 1051 | expect(whitelistBefore.length).toBe(0); 1052 | 1053 | if (isWhitelist) { 1054 | await vestingWallet.sendAddWhitelist(vestingSender.getSender(), { 1055 | queryId: 111, 1056 | value: toNano('1'), 1057 | addresses: [to] 1058 | }); 1059 | 1060 | expect(await vestingWallet.getIsWhitelisted(to)).toBeTruthy(); 1061 | } 1062 | 1063 | const stateInit = hasStateInit ? { 1064 | code, data: beginCell().endCell() 1065 | } : undefined; 1066 | 1067 | let body: Cell | undefined = undefined; 1068 | if (comment) { 1069 | body = (typeof comment === 'string') ? 1070 | beginCell().storeUint(0, 32).storeStringTail(comment as string).endCell() : 1071 | beginCell().storeUint(comment as number, 32).storeUint(0x456, 64).endCell(); 1072 | } 1073 | 1074 | const t = createWalletTransferV3({ 1075 | seqno: 0, 1076 | sendMode: sendMode, 1077 | walletId: 0, 1078 | messages: [ 1079 | senderArgsToMessageRelaxed({ 1080 | to: to, 1081 | value: toNano('3'), 1082 | bounce: bounceable, 1083 | body: body, 1084 | init: stateInit 1085 | }) 1086 | ], 1087 | secretKey: Buffer.from(new Uint8Array(64)) 1088 | }); 1089 | 1090 | const result = await vestingWallet.sendInternalTransfer(sender.getSender(), { 1091 | value: toNano('1'), 1092 | queryId: 567, 1093 | sendMode: sendMode, 1094 | msg: t.beginParse().loadRef() 1095 | }); 1096 | 1097 | expect(result.transactions).toHaveTransaction({ 1098 | from: owner.address, 1099 | to: vestingWallet.address, 1100 | success: true 1101 | }); 1102 | 1103 | expect(result.transactions).toHaveTransaction({ 1104 | from: vestingWallet.address, 1105 | to: owner.address, 1106 | body: beginCell().storeUint(Opcodes.send_response, 32).storeUint(567, 64).endCell() 1107 | }); 1108 | 1109 | expect(result.transactions).toHaveTransaction({ 1110 | from: vestingWallet.address, 1111 | to: to, 1112 | value: toNano('3'), 1113 | }); 1114 | expect(result.transactions.length).toBe(5); 1115 | 1116 | const lastTx: any = result.transactions[result.transactions.length - 2]; 1117 | expect(lastTx.inMessage.info.src.toString()).toBe(vestingWallet.address.toString()); 1118 | expect(lastTx.inMessage.info.dest.toString()).toBe(owner.address.toString()); 1119 | expect(lastTx.inMessage.info.value.coins).toBeLessThan(toNano('1')); 1120 | 1121 | const bouncedTx: any = result.transactions[result.transactions.length - 1]; 1122 | expect(bouncedTx.inMessage.info.src.toString()).toBe(to.toString()); 1123 | expect(bouncedTx.inMessage.info.dest.toString()).toBe(vestingWallet.address.toString()); 1124 | 1125 | const whitelist = await vestingWallet.getWhitelist(); 1126 | expect(whitelist.length).toBe(isWhitelist ? 1 : 0); 1127 | 1128 | if (isWhitelist) { 1129 | expect(await vestingWallet.getIsWhitelisted(to)).toBeTruthy(); 1130 | } 1131 | 1132 | await checkLockupData(); 1133 | } 1134 | 1135 | // after vesting 1136 | 1137 | it('if lock expired && to vestingSender - sendmode != 3, non-bounceable, state_init, bin comment allowed', async () => { 1138 | await transferAllow(VESTING_START_TIME + VESTING_TOTAL_DURATION + 1, owner, vestingSender.address, false, 1, false, true, 0x567); 1139 | }); 1140 | 1141 | it('if lock expired && to vestingSender - sendmode != 3, non-bounceable, state_init, "y" allowed', async () => { 1142 | await transferAllow(VESTING_START_TIME + VESTING_TOTAL_DURATION + 1, owner, vestingSender.address, false, 1, false, true, 'y'); 1143 | }); 1144 | 1145 | it('if lock expired && to anyone - sendmode != 3, non-bounceable, state_init, bin comment allowed', async () => { 1146 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1147 | await transferAllow(VESTING_START_TIME + VESTING_TOTAL_DURATION + 1, owner, notVestingSender.address, false, 1, false, true, 0x567); 1148 | }); 1149 | 1150 | it('if lock expired && to anyone - sendmode != 3, non-bounceable, state_init, "y" allowed', async () => { 1151 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1152 | await transferAllow(VESTING_START_TIME + VESTING_TOTAL_DURATION + 1, owner, notVestingSender.address, false, 1, false, true, 'y'); 1153 | }); 1154 | 1155 | it('if lock expired && to whitelist - sendmode != 3, non-bounceable, state_init, bin comment allowed', async () => { 1156 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1157 | await transferAllow(VESTING_START_TIME + VESTING_TOTAL_DURATION + 1, owner, notVestingSender.address, true, 1, false, true, 0x567); 1158 | }); 1159 | 1160 | it('if lock expired && to whitelist - sendmode != 3, non-bounceable, state_init, "y" allowed', async () => { 1161 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1162 | await transferAllow(VESTING_START_TIME + VESTING_TOTAL_DURATION + 1, owner, notVestingSender.address, true, 1, false, true, 'y'); 1163 | }); 1164 | 1165 | // to anyone 1166 | 1167 | it('if locked && to anyone - senmode != 3 rejected', async () => { 1168 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1169 | await transferReject(ErrorCodes.send_mode_not_allowed, owner, notVestingSender.address, false, 1, false, true, 0x567); 1170 | }); 1171 | 1172 | it('if locked && to anyone - non-bounceable, state_init, bin comment allowed', async () => { 1173 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1174 | await transferAllow(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, notVestingSender.address, false, 3, false, true, 0x567); 1175 | }); 1176 | 1177 | it('if locked && to anyone - non-bounceable, state_init, "y" allowed', async () => { 1178 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1179 | await transferAllow(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, notVestingSender.address, false, 3, false, true, 'y'); 1180 | }); 1181 | 1182 | // to vesting sender 1183 | 1184 | it('if locked && to vestingSender - sendmode != 3 rejected', async () => { 1185 | await transferReject(ErrorCodes.send_mode_not_allowed, owner, vestingSender.address, false, 128, true, false, undefined); 1186 | }); 1187 | 1188 | it('if locked && to vestingSender - non-bounceable, state_init, bin comment allowed', async () => { 1189 | await transferAllow(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, vestingSender.address, false, 3, false, true, 0x567); 1190 | }); 1191 | 1192 | it('if locked && to vestingSender - non-bounceable, state_init, "y" allowed', async () => { 1193 | await transferAllow(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, vestingSender.address, false, 3, false, true, 'y'); 1194 | }); 1195 | 1196 | // to whitelist 1197 | 1198 | it('if locked && whitelist - sendmode != 3 rejected', async () => { 1199 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1200 | await transferReject(ErrorCodes.send_mode_not_allowed, owner, notVestingSender.address, true, 128, true, false, undefined); 1201 | }); 1202 | 1203 | it('if locked && whitelist - non-bounceable rejected', async () => { 1204 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1205 | await transferReject(ErrorCodes.non_bounceable_not_allowed, owner, notVestingSender.address, true, 3, false, false, undefined); 1206 | }); 1207 | 1208 | it('if locked && whitelist - stateInit rejected', async () => { 1209 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1210 | await transferReject(ErrorCodes.state_init_not_allowed, owner, notVestingSender.address, true, 3, true, true, undefined); 1211 | }); 1212 | 1213 | it('if locked && whitelist - bin comment rejected', async () => { 1214 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1215 | await transferReject(ErrorCodes.comment_not_allowed, owner, notVestingSender.address, true, 3, true, false, 0x567); 1216 | }); 1217 | 1218 | it('if locked && whitelist & not elector - new_stake rejected', async () => { 1219 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1220 | await transferReject(ErrorCodes.comment_not_allowed, owner, notVestingSender.address, true, 3, true, false, 0x4e73744b); 1221 | }); 1222 | 1223 | it('if locked && whitelist & not elector - recover_stake rejected', async () => { 1224 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1225 | await transferReject(ErrorCodes.comment_not_allowed, owner, notVestingSender.address, true, 3, true, false, 0x47657424); 1226 | }); 1227 | 1228 | // elector 1229 | 1230 | it('if locked && whitelist & elector - empty rejected', async () => { 1231 | await transferReject(9, owner, ELECTOR_ADDRESS, true, 3, true, false, undefined); 1232 | }); 1233 | 1234 | it('if locked && whitelist & elector - "d" rejected', async () => { 1235 | await transferReject(ErrorCodes.comment_not_allowed, owner, ELECTOR_ADDRESS, true, 3, true, false, "d"); 1236 | }); 1237 | 1238 | it('if locked && whitelist & elector - "0x567" rejected', async () => { 1239 | await transferReject(ErrorCodes.comment_not_allowed, owner, ELECTOR_ADDRESS, true, 3, true, false, 0x567); 1240 | }); 1241 | 1242 | it('if locked && whitelist & elector - new_stake allowed', async () => { 1243 | await transferAllowElector(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, ELECTOR_ADDRESS, true, 3, true, false, Opcodes.elector_new_stake); 1244 | }); 1245 | 1246 | it('if locked && whitelist & elector - recover_stake allowed', async () => { 1247 | await transferAllowElector(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, ELECTOR_ADDRESS, true, 3, true, false, Opcodes.elector_recover_stake); 1248 | }); 1249 | 1250 | it('if locked && whitelist & elector - vote_for_complaint allowed', async () => { 1251 | await transferAllowElector(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, ELECTOR_ADDRESS, true, 3, true, false, Opcodes.vote_for_complaint); 1252 | }); 1253 | 1254 | it('if locked && whitelist & elector - vote_for_proposal allowed', async () => { 1255 | await transferAllowElector(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, ELECTOR_ADDRESS, true, 3, true, false, Opcodes.vote_for_proposal); 1256 | }); 1257 | 1258 | // config 1259 | 1260 | it('if locked && whitelist & config - empty rejected', async () => { 1261 | await transferReject(9, owner, CONFIG_ADDRESS, true, 3, true, false, undefined); 1262 | }); 1263 | 1264 | it('if locked && whitelist & config - "d" rejected', async () => { 1265 | await transferReject(ErrorCodes.comment_not_allowed, owner, CONFIG_ADDRESS, true, 3, true, false, "d"); 1266 | }); 1267 | 1268 | it('if locked && whitelist & config - "0x567" rejected', async () => { 1269 | await transferReject(ErrorCodes.comment_not_allowed, owner, CONFIG_ADDRESS, true, 3, true, false, 0x567); 1270 | }); 1271 | 1272 | it('if locked && whitelist & config - new_stake rejected', async () => { 1273 | await transferReject(ErrorCodes.comment_not_allowed, owner, CONFIG_ADDRESS, true, 3, true, false, Opcodes.elector_new_stake); 1274 | }); 1275 | 1276 | it('if locked && whitelist & config - recover_stake allowed', async () => { 1277 | await transferReject(ErrorCodes.comment_not_allowed, owner, CONFIG_ADDRESS, true, 3, true, false, Opcodes.elector_recover_stake); 1278 | }); 1279 | 1280 | it('if locked && whitelist & config - vote_for_complaint allowed', async () => { 1281 | await transferReject(ErrorCodes.comment_not_allowed, owner, CONFIG_ADDRESS, true, 3, true, false, Opcodes.vote_for_complaint); 1282 | }); 1283 | 1284 | it('if locked && whitelist & config - vote_for_proposal allowed', async () => { 1285 | await transferAllowElector(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, CONFIG_ADDRESS, true, 3, true, false, Opcodes.vote_for_proposal); 1286 | }); 1287 | 1288 | // 1289 | 1290 | it('if locked && whitelist - 0x1000 allowed', async () => { 1291 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1292 | await transferAllow(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, notVestingSender.address, true, 3, true, false, Opcodes.single_nominator_pool_withdraw); 1293 | }); 1294 | it('if locked && whitelist - 0x1001 allowed', async () => { 1295 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1296 | await transferAllow(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, notVestingSender.address, true, 3, true, false, Opcodes.single_nominator_pool_change_validator); 1297 | }); 1298 | it('if locked && whitelist - 0x47d54391 allowed', async () => { 1299 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1300 | await transferAllow(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, notVestingSender.address, true, 3, true, false, Opcodes.ton_stakers_deposit); 1301 | }); 1302 | it('if locked && whitelist - 0x595f07bc allowed', async () => { 1303 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1304 | await transferAllow(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, notVestingSender.address, true, 3, true, false, Opcodes.jetton_burn); 1305 | }); 1306 | 1307 | it('if locked && whitelist - tonstakers vote allowed', async () => { 1308 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1309 | await transferAllow(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, notVestingSender.address, true, 3, true, false, Opcodes.ton_stakers_vote); 1310 | }); 1311 | 1312 | it('if locked && whitelist - vote for complaint allowed', async () => { 1313 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1314 | await transferAllow(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, notVestingSender.address, true, 3, true, false, Opcodes.vote_for_complaint); 1315 | }); 1316 | 1317 | it('if locked && whitelist - vote for proposal allowed', async () => { 1318 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1319 | await transferAllow(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, notVestingSender.address, true, 3, true, false, Opcodes.vote_for_proposal); 1320 | }); 1321 | 1322 | it('if locked && whitelist - single-nominator send_raw_msg rejected', async () => { 1323 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1324 | await transferReject(ErrorCodes.comment_not_allowed, owner, notVestingSender.address, true, 3, true, false, 0x7702); 1325 | }); 1326 | 1327 | it('if locked && whitelist - single-nominator upgrade rejected', async () => { 1328 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1329 | await transferReject(ErrorCodes.comment_not_allowed, owner, notVestingSender.address, true, 3, true, false, 0x9903); 1330 | }); 1331 | 1332 | it('if locked && whitelist - new_stake rejected', async () => { 1333 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1334 | await transferReject(ErrorCodes.comment_not_allowed, owner, notVestingSender.address, true, 3, true, false, Opcodes.elector_new_stake); 1335 | }); 1336 | 1337 | it('if locked && whitelist - recover_stake rejected', async () => { 1338 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1339 | await transferReject(ErrorCodes.comment_not_allowed, owner, notVestingSender.address, true, 3, true, false, Opcodes.elector_recover_stake); 1340 | }); 1341 | 1342 | it('if locked && whitelist - "y" rejected', async () => { 1343 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1344 | await transferReject(ErrorCodes.symbols_not_allowed, owner, notVestingSender.address, true, 3, true, false, 'y'); 1345 | }); 1346 | 1347 | it('if locked && whitelist - empty allowed', async () => { 1348 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1349 | await transferAllow(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, notVestingSender.address, true, 3, true, false, undefined); 1350 | }); 1351 | it('if locked && whitelist - "" allowed', async () => { 1352 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1353 | await transferAllow(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, notVestingSender.address, true, 3, true, false, ''); 1354 | }); 1355 | it('if locked && whitelist - "d" allowed', async () => { 1356 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1357 | await transferAllow(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, notVestingSender.address, true, 3, true, false, 'd'); 1358 | }); 1359 | it('if locked && whitelist - "w" allowed', async () => { 1360 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1361 | await transferAllow(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, notVestingSender.address, true, 3, true, false, 'w'); 1362 | }); 1363 | it('if locked && whitelist - "D" allowed', async () => { 1364 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1365 | await transferAllow(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, notVestingSender.address, true, 3, true, false, 'D'); 1366 | }); 1367 | it('if locked && whitelist - "W" allowed', async () => { 1368 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1369 | await transferAllow(VESTING_START_TIME + UNLOCK_PERIOD * 7, owner, notVestingSender.address, true, 3, true, false, 'W'); 1370 | }); 1371 | 1372 | // external 1373 | 1374 | it('external transfer from owner', async () => { 1375 | // blockchain.now = time; 1376 | 1377 | const whitelistBefore = await vestingWallet.getWhitelist(); 1378 | expect(whitelistBefore.length).toBe(0); 1379 | 1380 | const transferResult = await vestingWallet.sendExternalTransfer({ 1381 | seqno: 0, 1382 | secretKey: ownerKeyPair.secretKey, 1383 | messages: [ 1384 | senderArgsToMessageRelaxed({ 1385 | to: vestingSender.address, 1386 | value: toNano('122999.9'), 1387 | bounce: true, 1388 | }) 1389 | ], 1390 | sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, 1391 | timeout: 60, 1392 | subWalletId: SUB_WALLET_ID 1393 | }); 1394 | 1395 | expect(transferResult.transactions).toHaveTransaction({ 1396 | from: vestingWallet.address, 1397 | to: vestingSender.address, 1398 | value: toNano('122999.9'), 1399 | }) 1400 | expect(transferResult.transactions.length).toBe(2); 1401 | 1402 | await checkLockupData(1); 1403 | 1404 | const whitelist = await vestingWallet.getWhitelist(); 1405 | expect(whitelist.length).toBe(0); 1406 | }); 1407 | 1408 | it('external transfer from owner2', async () => { 1409 | blockchain.now = VESTING_START_TIME + VESTING_TOTAL_DURATION + 1; 1410 | const notVestingSender = await blockchain.treasury('notVestingSender'); 1411 | 1412 | const whitelistBefore = await vestingWallet.getWhitelist(); 1413 | expect(whitelistBefore.length).toBe(0); 1414 | 1415 | const transferResult = await vestingWallet.sendExternalTransfer({ 1416 | seqno: 0, 1417 | secretKey: ownerKeyPair.secretKey, 1418 | messages: [ 1419 | senderArgsToMessageRelaxed({ 1420 | to: notVestingSender.address, 1421 | value: toNano('122999.9'), 1422 | bounce: false, 1423 | body: beginCell().storeUint(0, 32).storeStringTail('y').endCell() 1424 | }) 1425 | ], 1426 | sendMode: SendMode.PAY_GAS_SEPARATELY, 1427 | timeout: 60, 1428 | subWalletId: SUB_WALLET_ID 1429 | }); 1430 | 1431 | expect(transferResult.transactions).toHaveTransaction({ 1432 | from: vestingWallet.address, 1433 | to: notVestingSender.address, 1434 | value: toNano('122999.9'), 1435 | }) 1436 | expect(transferResult.transactions.length).toBe(2); 1437 | 1438 | await checkLockupData(1); 1439 | 1440 | const whitelist = await vestingWallet.getWhitelist(); 1441 | expect(whitelist.length).toBe(0); 1442 | }); 1443 | 1444 | it('external transfer deploy', async () => { 1445 | // blockchain.now = time; 1446 | 1447 | const whitelistBefore = await vestingWallet.getWhitelist(); 1448 | expect(whitelistBefore.length).toBe(0); 1449 | 1450 | const transferResult = await vestingWallet.sendExternalTransfer({ 1451 | seqno: 0, 1452 | secretKey: ownerKeyPair.secretKey, 1453 | messages: [], 1454 | sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, 1455 | timeout: 60, 1456 | subWalletId: SUB_WALLET_ID 1457 | }); 1458 | 1459 | expect(transferResult.transactions.length).toBe(1); 1460 | 1461 | await checkLockupData(1); 1462 | 1463 | const whitelist = await vestingWallet.getWhitelist(); 1464 | expect(whitelist.length).toBe(0); 1465 | }); 1466 | 1467 | it('external transfer no balance and no ingnore errors', async () => { 1468 | // blockchain.now = time; 1469 | 1470 | const whitelistBefore = await vestingWallet.getWhitelist(); 1471 | expect(whitelistBefore.length).toBe(0); 1472 | 1473 | const transferResult = await vestingWallet.sendExternalTransfer({ 1474 | seqno: 0, 1475 | secretKey: ownerKeyPair.secretKey, 1476 | messages: [ 1477 | senderArgsToMessageRelaxed({ 1478 | to: vestingSender.address, 1479 | value: toNano('50000000000'), 1480 | bounce: true 1481 | }) 1482 | ], 1483 | sendMode: SendMode.PAY_GAS_SEPARATELY, 1484 | timeout: 60, 1485 | subWalletId: SUB_WALLET_ID 1486 | }); 1487 | 1488 | expect(transferResult.transactions.length).toBe(1); 1489 | 1490 | await checkLockupData(1); 1491 | 1492 | const whitelist = await vestingWallet.getWhitelist(); 1493 | expect(whitelist.length).toBe(0); 1494 | }); 1495 | 1496 | it('external - invalid signature', async () => { 1497 | // blockchain.now = time; 1498 | 1499 | const whitelistBefore = await vestingWallet.getWhitelist(); 1500 | expect(whitelistBefore.length).toBe(0); 1501 | 1502 | let wasError = false; 1503 | 1504 | try { 1505 | const transferResult = await vestingWallet.sendExternalTransfer({ 1506 | seqno: 0, 1507 | secretKey: notOwnerKeyPair.secretKey, 1508 | messages: [ 1509 | senderArgsToMessageRelaxed({ 1510 | to: vestingSender.address, 1511 | value: toNano('2'), 1512 | bounce: true 1513 | }) 1514 | ], 1515 | sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, 1516 | timeout: 60, 1517 | subWalletId: SUB_WALLET_ID 1518 | }); 1519 | } catch (e) { 1520 | wasError = true; 1521 | } 1522 | 1523 | expect(wasError).toBeTruthy() 1524 | 1525 | await checkLockupData(0); 1526 | 1527 | const whitelist = await vestingWallet.getWhitelist(); 1528 | expect(whitelist.length).toBe(0); 1529 | }); 1530 | 1531 | it('external - invalid seqno', async () => { 1532 | // blockchain.now = time; 1533 | 1534 | const whitelistBefore = await vestingWallet.getWhitelist(); 1535 | expect(whitelistBefore.length).toBe(0); 1536 | 1537 | let wasError = false; 1538 | 1539 | try { 1540 | const transferResult = await vestingWallet.sendExternalTransfer({ 1541 | seqno: 123, 1542 | secretKey: ownerKeyPair.secretKey, 1543 | messages: [ 1544 | senderArgsToMessageRelaxed({ 1545 | to: vestingSender.address, 1546 | value: toNano('2'), 1547 | bounce: true 1548 | }) 1549 | ], 1550 | sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, 1551 | timeout: 60, 1552 | subWalletId: SUB_WALLET_ID 1553 | }); 1554 | } catch (e) { 1555 | wasError = true; 1556 | } 1557 | 1558 | expect(wasError).toBeTruthy() 1559 | 1560 | await checkLockupData(0); 1561 | 1562 | const whitelist = await vestingWallet.getWhitelist(); 1563 | expect(whitelist.length).toBe(0); 1564 | }); 1565 | 1566 | it('external - invalid subwalletId', async () => { 1567 | // blockchain.now = time; 1568 | 1569 | const whitelistBefore = await vestingWallet.getWhitelist(); 1570 | expect(whitelistBefore.length).toBe(0); 1571 | 1572 | let wasError = false; 1573 | 1574 | try { 1575 | const transferResult = await vestingWallet.sendExternalTransfer({ 1576 | seqno: 0, 1577 | secretKey: ownerKeyPair.secretKey, 1578 | messages: [ 1579 | senderArgsToMessageRelaxed({ 1580 | to: vestingSender.address, 1581 | value: toNano('2'), 1582 | bounce: true 1583 | }) 1584 | ], 1585 | sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, 1586 | timeout: 60, 1587 | subWalletId: SUB_WALLET_ID + 1 1588 | }); 1589 | } catch (e) { 1590 | wasError = true; 1591 | } 1592 | 1593 | expect(wasError).toBeTruthy() 1594 | 1595 | await checkLockupData(0); 1596 | 1597 | const whitelist = await vestingWallet.getWhitelist(); 1598 | expect(whitelist.length).toBe(0); 1599 | }); 1600 | 1601 | // todo: external - invalid valid_until - should be no changes 1602 | 1603 | }); 1604 | -------------------------------------------------------------------------------- /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/VestingWallet.compile.ts: -------------------------------------------------------------------------------- 1 | import { CompilerConfig } from '@ton-community/blueprint'; 2 | 3 | export const compile: CompilerConfig = { 4 | lang: 'func', 5 | targets: ['contracts/vesting_wallet.fc'], 6 | }; 7 | -------------------------------------------------------------------------------- /wrappers/VestingWallet.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | beginCell, 4 | Cell, 5 | Contract, 6 | contractAddress, 7 | ContractProvider, 8 | MessageRelaxed, 9 | Sender, 10 | SendMode 11 | } from 'ton-core'; 12 | import {Maybe} from "ton/dist/utils/maybe"; 13 | import {createWalletTransferV3} from "ton/dist/wallets/signing/createWalletTransfer"; 14 | 15 | export type VestingWalletConfig = { 16 | subWalletId: number, 17 | publicKeyHex: string; 18 | 19 | vestingStartTime: number; 20 | vestingTotalDuration: number; 21 | unlockPeriod: number; 22 | cliffDuration: number; 23 | vestingTotalAmount: bigint; 24 | 25 | vestingSenderAddress: Address; 26 | ownerAddress: Address; 27 | }; 28 | 29 | export function vestingWalletConfigToCell(config: VestingWalletConfig): Cell { 30 | return beginCell() 31 | .storeUint(0, 32) // seqno 32 | .storeUint(config.subWalletId, 32) // subwallet 33 | .storeBuffer(Buffer.from(config.publicKeyHex, 'hex')) // public_key 34 | .storeUint(0, 1) // empty whitelist 35 | .storeRef( 36 | beginCell() 37 | .storeUint(config.vestingStartTime, 64) 38 | .storeUint(config.vestingTotalDuration, 32) 39 | .storeUint(config.unlockPeriod, 32) 40 | .storeUint(config.cliffDuration, 32) 41 | .storeCoins(config.vestingTotalAmount) 42 | .storeAddress(config.vestingSenderAddress) 43 | .storeAddress(config.ownerAddress) 44 | .endCell() 45 | ) 46 | .endCell(); 47 | } 48 | 49 | export const Opcodes = { 50 | add_whitelist: 0x7258a69b, 51 | add_whitelist_response: 0xf258a69b, 52 | send: 0xa7733acd, 53 | send_response: 0xf7733acd, 54 | 55 | elector_new_stake: 0x4e73744b, 56 | elector_recover_stake: 0x47657424, 57 | vote_for_complaint: 0x56744370, 58 | vote_for_proposal: 0x566f7465, 59 | 60 | single_nominator_pool_withdraw: 0x1000, 61 | single_nominator_pool_change_validator: 0x1001, 62 | 63 | ton_stakers_deposit: 0x47d54391, 64 | jetton_burn: 0x595f07bc, 65 | ton_stakers_vote: 0x69fb306c, 66 | }; 67 | 68 | export const ErrorCodes = { 69 | expired: 36, 70 | invalid_seqno: 33, 71 | invalid_subwallet_id: 34, 72 | invalid_signature: 35, 73 | 74 | send_mode_not_allowed: 100, 75 | non_bounceable_not_allowed: 101, 76 | state_init_not_allowed: 102, 77 | comment_not_allowed: 103, 78 | symbols_not_allowed: 104, 79 | }; 80 | 81 | export class VestingWallet implements Contract { 82 | constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) { 83 | } 84 | 85 | static createFromAddress(address: Address) { 86 | return new VestingWallet(address); 87 | } 88 | 89 | static createFromConfig(config: VestingWalletConfig, code: Cell, workchain = 0) { 90 | const data = vestingWalletConfigToCell(config); 91 | const init = {code, data}; 92 | return new VestingWallet(contractAddress(workchain, init), init); 93 | } 94 | 95 | async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { 96 | await provider.internal(via, { 97 | value, 98 | sendMode: SendMode.PAY_GAS_SEPARATELY, 99 | body: beginCell().endCell(), 100 | }); 101 | } 102 | 103 | async sendSimple( 104 | provider: ContractProvider, 105 | via: Sender, 106 | opts: { 107 | value: bigint; 108 | comment?: string; 109 | } 110 | ) { 111 | await provider.internal(via, { 112 | value: opts.value, 113 | sendMode: SendMode.PAY_GAS_SEPARATELY, 114 | body: opts.comment ? beginCell().storeUint(0, 32).storeStringTail(opts.comment).endCell() : undefined 115 | }); 116 | } 117 | 118 | async sendOp( 119 | provider: ContractProvider, 120 | via: Sender, 121 | opts: { 122 | value: bigint; 123 | op: number; 124 | } 125 | ) { 126 | await provider.internal(via, { 127 | value: opts.value, 128 | sendMode: SendMode.PAY_GAS_SEPARATELY, 129 | body: beginCell().storeUint(opts.op, 32).endCell() 130 | }); 131 | } 132 | 133 | async sendInternalTransfer( 134 | provider: ContractProvider, 135 | via: Sender, 136 | opts: { 137 | value: bigint; 138 | sendMode: number; 139 | msg: Cell, 140 | queryId?: number; 141 | } 142 | ) { 143 | await provider.internal(via, { 144 | value: opts.value, 145 | sendMode: SendMode.PAY_GAS_SEPARATELY, 146 | body: beginCell() 147 | .storeUint(Opcodes.send, 32) 148 | .storeUint(opts.queryId || 0, 64) 149 | .storeUint(opts.sendMode, 8) 150 | .storeRef(opts.msg) 151 | .endCell() 152 | }); 153 | } 154 | 155 | /** 156 | * Create transfer 157 | */ 158 | createTransfer(args: { seqno: number, sendMode: SendMode, secretKey: Buffer, messages: MessageRelaxed[], timeout: Maybe, subWalletId: number }) { 159 | return createWalletTransferV3({ 160 | seqno: args.seqno, 161 | sendMode: args.sendMode, 162 | secretKey: args.secretKey, 163 | messages: args.messages, 164 | timeout: args.timeout, 165 | walletId: args.subWalletId 166 | }); 167 | } 168 | 169 | /** 170 | * Sign and send external transfer 171 | */ 172 | async sendExternalTransfer(provider: ContractProvider, args: { 173 | seqno: number, 174 | secretKey: Buffer, 175 | messages: MessageRelaxed[], 176 | sendMode: SendMode, 177 | timeout: number, 178 | subWalletId: number 179 | }) { 180 | let transfer = this.createTransfer(args); 181 | await provider.external(transfer) 182 | } 183 | 184 | static createAddWhitelistBody(addresses: Address[], queryId?: number): Cell { 185 | const root = beginCell() 186 | .storeUint(Opcodes.add_whitelist, 32) // op 187 | .storeUint(queryId || 0, 64) // query_id; 188 | .storeAddress(addresses[0]); 189 | 190 | let cell: Cell | null = null; 191 | 192 | for (let i = addresses.length - 1; i >= 1; i--) { 193 | const newCell = beginCell().storeAddress(addresses[i]); 194 | 195 | if (cell) { 196 | newCell.storeRef(cell); 197 | } 198 | 199 | cell = newCell.endCell(); 200 | } 201 | 202 | if (cell) { 203 | root.storeRef(cell); 204 | } 205 | 206 | return root.endCell(); 207 | } 208 | 209 | async sendAddWhitelist( 210 | provider: ContractProvider, 211 | via: Sender, 212 | opts: { 213 | value: bigint; 214 | queryId?: number; 215 | addresses: Address[]; 216 | } 217 | ) { 218 | await provider.internal(via, { 219 | value: opts.value, 220 | sendMode: SendMode.PAY_GAS_SEPARATELY, 221 | body: VestingWallet.createAddWhitelistBody(opts.addresses, opts.queryId) 222 | }); 223 | } 224 | 225 | async getSeqno(provider: ContractProvider): Promise { 226 | const result = await provider.get('seqno', []); 227 | return result.stack.readNumber(); 228 | } 229 | 230 | async getSubWalletId(provider: ContractProvider): Promise { 231 | const result = await provider.get('get_subwallet_id', []); 232 | return result.stack.readNumber(); 233 | } 234 | 235 | async getPublicKeyHex(provider: ContractProvider): Promise { 236 | const result = await provider.get('get_public_key', []); 237 | return result.stack.readBigNumber().toString(16); 238 | } 239 | 240 | async getVestingData(provider: ContractProvider) { 241 | const result = await provider.get('get_vesting_data', []); 242 | return { 243 | vestingStartTime: result.stack.readNumber(), 244 | vestingTotalDuration: result.stack.readNumber(), 245 | unlockPeriod: result.stack.readNumber(), 246 | cliffDuration: result.stack.readNumber(), 247 | vestingTotalAmount: result.stack.readBigNumber(), 248 | vestingSenderAddress: result.stack.readAddress(), 249 | ownerAddress: result.stack.readAddress(), 250 | whitelistCell: result.stack.readCellOpt() 251 | }; 252 | } 253 | 254 | async getWhitelist(provider: ContractProvider): Promise { 255 | const result = await provider.get('get_whitelist', []); 256 | let addresses = []; 257 | let list = result.stack.readTupleOpt(); 258 | while (list) { 259 | const tuple = list.readTuple(); 260 | const wc = tuple.readNumber(); 261 | const hash = tuple.readBigNumber(); 262 | addresses.push(Address.parse(wc + ':' + hash.toString(16).padStart(64, '0'))); 263 | if (list.remaining > 0) { 264 | list = list.readTupleOpt(); 265 | } 266 | } 267 | return addresses; 268 | } 269 | 270 | async getIsWhitelisted(provider: ContractProvider, address: Address): Promise { 271 | const result = await provider.get('is_whitelisted', [{ 272 | type: 'slice', 273 | cell: beginCell().storeAddress(address).endCell() 274 | }]); 275 | return result.stack.readBoolean(); 276 | } 277 | 278 | async getLockedAmount(provider: ContractProvider, time: number): Promise { 279 | const result = await provider.get('get_locked_amount', [{ 280 | type: 'int', 281 | value: BigInt(time) 282 | }]); 283 | return result.stack.readBigNumber(); 284 | } 285 | } 286 | --------------------------------------------------------------------------------