├── .env.example
├── .gitignore
├── .idea
├── .gitignore
├── Multiowner.iml
├── misc.xml
├── modules.xml
└── vcs.xml
├── .prettierignore
├── .prettierrc
├── README.md
├── TESTING.md
├── audits
├── 202403TON_Foundation_Multisignature_Wallet_Report_+_Fix_Review.pdf
└── Multisig_Zellic_Audit_Report.pdf
├── build
├── Librarian.compiled.json
├── Multisig.compiled.json
└── Order.compiled.json
├── contracts
├── auto
│ └── order_code.func
├── errors.func
├── helper
│ └── librarian.func
├── imports
│ └── stdlib.fc
├── messages.func
├── multisig.func
├── multisig.tlb
├── op-codes.func
├── order.func
├── order_helpers.func
└── types.func
├── description.md
├── gasUtils.ts
├── jest.config.ts
├── package-lock.json
├── package.json
├── scripts
├── deployLibrary.ts
├── deployMultiownerWallet.ts
└── newOrder.ts
├── tests
├── FeeComputation.spec.ts
├── Multisig.spec.ts
├── Order.spec.ts
└── utils.ts
├── tsconfig.json
└── wrappers
├── Constants.ts
├── Librarian.compile.ts
├── Librarian.ts
├── Multisig.compile.ts
├── Multisig.ts
├── Order.compile.ts
└── Order.ts
/.env.example:
--------------------------------------------------------------------------------
1 | WALLET_MNEMONIC=
2 | WALLET_VERSION=v3r2
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | temp
3 | contracts/auto
4 | .env
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/Multiowner.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | build
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "tabWidth": 4,
4 | "singleQuote": true,
5 | "bracketSpacing": true,
6 | "semi": true
7 | }
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Multisignature Wallet
2 |
3 | This set of contracts provide "N-of-M multisig" functionality: at least N parties out of predefined set of M _signers_ must approve **Order** to execute it.
4 |
5 | Each **Order** may contain arbitrary number of actions: outgoing messages and updates of parameters. Since content of the messages is arbitrary **Order** may execute arbitrary high-level interactions on TON: sending TONs, sending/minting jettons, execute administrative duty, etc.
6 |
7 | > ⚠️ Multisig does not limit the content of Order actions, so Order can include absolutely any actions, including those that create new multisig orders or approve existing multisg orders or change multisig configuration (e.g. a list of signers).
8 | >
9 | > UI and other tools for working with multisig must fully parse the contents of orders and clearly report all actions that will be performed by the order. Such tools should also explicitly report parsing errors or actions of an unknown type.
10 | >
11 | > Singers must approve order only after fully reading order contents.
12 |
13 | > ⚠️ The multisig UI should display all created and unexecuted orders (these can be found in outgoing messages from multisig), as well as the match of their list of signers with the current list of singers of multisig, so that users clearly see all active orders that can be executed.
14 |
15 | Parameters, such as threshold N, list of _signers_ and other can only be updated by consensus of current N-of-M owners.
16 |
17 | Any _signer_ may propose new **Order**. Multisignature wallet also allows to assign _proposer_ role: _proposer_ may suggest new Orders but can not approve them.
18 |
19 | Each **Order** has expiration date after which it can not be executed.
20 |
21 | Each _signer_ may be wallet, hardware wallet, multisig themselves as well as other smart-contracts with its own logic.
22 |
23 | This Multisignature wallet was developed keeping in mind [Safe{Wallet}](https://app.safe.global/welcome).
24 |
25 | ## Guarantees
26 |
27 | - Nobody except _proposers_ and _signers_ can initiate creation of new order, nobody except _signers_ can approve new order.
28 | - Change of the _signers_ set invalidates all orders with other set. More strictly, Order is only valid when current _signers_ of the Multisig are equal to _signers_ of the Order.
29 | - _Signer_ compromise, in particularly compromise of less than _signers.length - N_, does not hinder to execute orders or to propose new ones (including orders which will remove compromised _signers_ from the signers list)
30 | - _Proposer_ compromise does not hinder to execute orders or to propose new ones (including orders which will remove compromised _proposer_ from the proposers list)
31 | - Logic of multisignature wallet can not be changed after deploy
32 |
33 | ## Architecture
34 | Whole system consists of four parts:
35 | * Signers - independent actors who approves orders execution
36 | * Proposers - helper actors who may propose new orders for execution
37 | * Multisig - contract that execute approved orders, thus it is address which will own assets and permissions; Multisig contract also store information on number of orders, current Signers and Proposers sets
38 | * Orders - child contracts, each of them holds information on one order: content of the order and approvals
39 |
40 | Flow is as follows:
41 | 1) proposer of new order (address from Proposers or Signers sets) build new order which consist of arbitrary number transfers from Multisig address and sends request to Multisig to start approval of this order
42 | 2) Multisig receives the request, check that it is sent from authorized actor and deploy child sub-contract Order which holds order content
43 | 3) Signers independently send approval messages to Order contract
44 | 4) Once Order gets enough approvals it sends request to execute order to Multisig
45 | 5) Multisig authenticate Order (that it is indeed sent by Order and not by somebody else) as well as that set of Signers is still relevant and execute order (sends transfers from order)
46 | 6) If Order needs to have more than 255 transfers (limit of transfers in one tx), excessive transactions may be packed in last transfer from Multisig to itself as `internal_execute`
47 | 7) Multisig receives `internal_execute`, checks that it is sent from itself and continue execution.
48 |
49 | All fees on processing order (except order execution itself): creation Order contract and it's storage fees are borne by the actor who propose this order (whether it's Proposer or Signer).
50 |
51 | Besides transfers, Order may also contain Multisig Update Requests
52 |
53 |
54 |
55 | ## Project structure
56 |
57 | - `contracts` - source code of all the smart contracts of the project and their dependencies.
58 | - `wrappers` - wrapper classes (implementing `Contract` from ton-core) for the contracts, including any [de]serialization primitives and compilation functions.
59 | - `tests` - tests for the contracts.
60 | - `scripts` - scripts used by the project, mainly the deployment scripts.
61 |
62 | ## How to use
63 |
64 | ### Build
65 |
66 | `npx blueprint build` or `yarn blueprint build`
67 |
68 | ### Test
69 |
70 | `npx blueprint test` or `yarn blueprint test`
71 |
72 | ### Deploy or run another script
73 |
74 | `npx blueprint run` or `yarn blueprint run`
75 |
76 | use Toncenter API:
77 |
78 | `npx blueprint run --custom https://testnet.toncenter.com/api/v2/ --custom-version v2 --custom-type testnet --custom-key `
79 |
80 | API_KEY can be obtained on https://toncenter.com or https://testnet.toncenter.com
81 |
82 |
83 | ## Notes
84 |
85 | - Threshold must be > 0 and <= signers_num.
86 |
87 | - By design orders smart contract are not notified of multisig configuration update (signer, proposers, threshold).
88 | Such an order will continue to accept approvals, but when executed, it will be rejected because the multisig configuration has changed.
89 |
90 | - TON balance of an expired order can be returned to multisig. To do this, the order must collect enough approvals - it will be sent for execution, there will be no execution, but the TONs will be returned to the multisig.
91 |
92 | - `approve_accepted` auxiliary notification is not sent if the order is initialized and executed immediately (approve_on_init with threshold = 1).
93 |
94 | ## Security
95 |
96 | The multisig contract has been created by TON Core team and audited by security companies:
97 |
98 | - Zellic: [Audit Report](https://github.com/ton-blockchain/multisig-contract-v2/blob/master/audits/Multisig_Zellic_Audit_Report.pdf)
99 | - Trail of Bits: [Audit Report](https://github.com/ton-blockchain/multisig-contract-v2/blob/master/audits/202403TON_Foundation_Multisignature_Wallet_Report_%2B_Fix_Review.pdf)
100 |
101 | Feel free to review these reports for a detailed understanding of the contract's security measures.
--------------------------------------------------------------------------------
/TESTING.md:
--------------------------------------------------------------------------------
1 | # Testing plan
2 |
3 | ## Glossary
4 |
5 | ### List
6 |
7 | What i refer to list(for simplicity) in this document, is in fact dictionary with sequentially ordered indexes.
8 | All of such structures (`signers`, `proposers`, `order`) should be checked for indexes order.
9 | If list is not compliant with expected format, `error::invalid_dictionary_sequence` should be thrown.
10 |
11 | ### Signers
12 |
13 | Signers is a list of wallet addresses of `multiowner` wallet contract owners.
14 | Each signer can create new order and vote for approval
15 |
16 | ### Proposers
17 |
18 | Proposers is the list of wallet addresses allowed to only create new orders, but **can't vote for approval**.
19 |
20 | ## Multiowner testing
21 |
22 | ## New order
23 |
24 | - Only proposers and signers should be able to create new order.
25 | - Order expiration time should exceed current time.
26 | - Incoming value should be >= expected processing cost.
27 | - Order can't be empty.
28 | - Only successful `new_order` execution should produce deploy message for `order` contract.
29 | - Deploy message should result in successful `order` deployment with matching *threshold, signers, number of signers, expiration date and order body*.
30 | - Only successful `new_order` execution should result in `order_seqno` increase.
31 |
32 | ### Execute order
33 |
34 | - Only `order` contract with according `order_seqno` and `signers` hash (specified in message) should be able to trigger that operation.
35 | - `signers` cell hash in incoming message should match current state `signers` hash. Should guarantee that change of signers results in orders invalidation.
36 | - Should trigger actions processing sequentially according to order list.
37 | - Minimal processing cost calculated at `New order` stage should be >= actual costs of executing order including storage.
38 |
39 | ### Execute internal order
40 |
41 | - Should only be able to trigger from self address.
42 | - Main intention is to allow chained order execution.
43 | - Should trigger actions processing according to passed order list.
44 |
45 | ### Order processing
46 |
47 | Execute order message contains list with actions of two(currenty) types:
48 |
49 | - Outgoing message.
50 | - Update multisig parameters.
51 |
52 | #### Outgoing message
53 |
54 | Specifies message mode and message cell.
55 | Results in according message being sent.
56 |
57 | #### Update multiowner marameters
58 |
59 | Specifies multiowner state parameters such as:
60 |
61 | - Order threshold
62 | - Signers list
63 | - Proposers list
64 | - Modules prefix dictionary
65 | - Guard contract cell
66 |
67 | Should result in according contract state changes.
68 |
69 | ### Experimental
70 |
71 | All features below should only be accessible when contract is deployed with `EXPERIMENTAL_FEATURES` flag set.
72 |
73 | #### Module functionality execution
74 |
75 | - Module should be present in modules prefix dictionary by sender address used as a key.
76 | - Should result in module order being processed
77 |
78 | #### Guard functionality execution
79 |
80 | Guard is separate contract with it's own state (data/code) main purpose of which is to check execution context prior to action phase and react accordingly.
81 | For instance, prevent forbidden actions from execution.(?)
82 |
83 | ## Guarantee cases
84 |
85 | - Nobody except `proposers` and `owners` can initiate creation of new order, nobody except `owners` can approve new order.
86 | - Chenge of the `owners` set invalidates all orders with old set.
87 | - `Owner` compromise, in particularly compromise of less than N `owners`, does not hinder to execute orders or to propose new ones (including orders which will remove compromised `owners` from the owners list)
88 | - `Proposer` compromise does not hinder to execute orders or to propose new ones (including orders which will remove compromised `proposer` from the proposers list)
89 | - Logic of multiapproval wallet can not changed
90 |
91 | ## Order contract testing
92 |
93 | ### Initialization
94 |
95 | - Order contract should only be able to initialize once.
96 | - Order contract should only accept initialization messages from `multiowner` address.
97 | - Execution threshold should be set according to init message.
98 | - Signers list should be set according to init message.
99 | - Expiration date should exceed current timestamp(Should not be expired at the time of receiving a message).
100 | - Expiration date should be set according to init message.
101 | - Execution and approval state fields such as `approvals`(bit mask), `approvals_num`, `executed` should be zeroed out.
102 | - If signer initiated order contract deployment, it's approval should be accounted for. In case `threshold = 1` order should get executed.
103 |
104 | ### Order approval
105 |
106 | Approval state is described with following fields:
107 |
108 | - `approvals` is a bit mask where each bit describes signed position(bit position) and bit value describes presence of approval(true -> approved).
109 | - `approvals_num` is an approval counter that is compared against `threshold` during order execution check.
110 |
111 | In case approval is granted, bit is set in `approvals` mask in accordance with signer position in `signers` list.
112 |
113 | For approval to be granted:
114 |
115 | - Sender address should be present in `signers` list.
116 | - Signer index specified in message, should match sender address position at `signers` list.
117 | - Order should not be expired (`expiration_date < now()`).
118 | - Order can only be executed once `executed` field should be `false`.
119 | - Signer at specified index in `approvals` mask has not granted approval yet.(`error::already_approved`)
120 |
121 | In case order is expired:
122 |
123 | - Message carrying remaining value and indicating expiry is sent back to approval sender address.
124 | - Message carrying all of the order contract remaining balance is sent back to the `multiowner` contract.
125 |
126 | In case order is already executed, message carrying remaining value is sent back to approval sender address.
127 |
128 | ### Order execution
129 |
130 | On every initialization or order approval action, contract check if it's possible to execute order (order count has reached threshold).
131 | If so:
132 |
133 | - `op::execute` message, carrying all remaining balance is sent to `multiowner` contract.
134 | - `executed` flag is set to true and repeated execution should not be possible afterwards.
135 |
--------------------------------------------------------------------------------
/audits/202403TON_Foundation_Multisignature_Wallet_Report_+_Fix_Review.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ton-blockchain/multisig-contract-v2/9a4b13df6345c9c4068ca725e434b40f9ea5ca28/audits/202403TON_Foundation_Multisignature_Wallet_Report_+_Fix_Review.pdf
--------------------------------------------------------------------------------
/audits/Multisig_Zellic_Audit_Report.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ton-blockchain/multisig-contract-v2/9a4b13df6345c9c4068ca725e434b40f9ea5ca28/audits/Multisig_Zellic_Audit_Report.pdf
--------------------------------------------------------------------------------
/build/Librarian.compiled.json:
--------------------------------------------------------------------------------
1 | {"hex":"b5ee9c72410104010099000114ff00f4a413f4bcf2c80b01037ed33031d0d3030130fa4030ed44f807218103e8f94130f8075003a17ff83b02821012cc03007ff837a08010fb020170c880108306db3c72fb0688fb0488ed54030202000000888e40c85801cb055004cf1658fa02547120ed44ed45ed479d5bc85003cf17c9127158cb6acced67ed65ed64737fed11977001cb6a01cf17ed41edf101f2ffc901fb00db05afd31959"}
--------------------------------------------------------------------------------
/build/Multisig.compiled.json:
--------------------------------------------------------------------------------
1 | {"hex":"b5ee9c7241021201000495000114ff00f4a413f4bcf2c80b010201620802020120060302016605040159b0c9fe0a00405c00b21633c5804072fff26208b232c07d003d0032c0325c007e401d3232c084b281f2fff274201100f1b0cafb513434ffc04074c1c0407534c1c0407d01348000407448dfdc2385d4449e3d1f1be94c886654c0aebcb819c0a900b7806cc4b99b08548c2ebcb81b085fdc2385d4449e3d1f1be94c886654c0aebcb819c0a900b7806cc4b99b084c08b0803cb81b8930803cb81b5490eefcb81b40648cdfe440f880e00143bf74ff6a26869ff8080e9838080ea69838080fa0269000080e8881aaf8280fc11d0c0700c2f80703830cf94130038308f94130f8075006a18127f801a070f83681120670f836a0812bec70f836a0811d9870f836a022a60622a081053926a027a070f83823a481029827a070f838a003a60658a08106e05005a05005a0430370f83759a001a002cad033d0d3030171b0925f03e0fa403022d749c000925f03e002d31f0120c000925f04e001d33f01ed44d0d3ff0101d3070101d4d3070101f404d2000101d1288210f718510fbae30f054443c8500601cbff500401cb0712cc0101cb07f4000101ca00c9ed540d09029a363826821075097f5dba8eba068210a32c59bfba8ea9f82818c705f2e06503d4d1103410364650f8007f8e8d2178f47c6fa5209132e30d01b3e65b10355034923436e2505413e30d40155033040b0a02e23604d3ff0101d32f0101d3070101d3ff0101d4d1f8285005017002c858cf160101cbffc98822c8cb01f400f400cb00c97001f90074c8cb0212ca07cbffc9d01bc705f2e06526f9001aba5193be19b0f2e06607f823bef2e06f44145056f8007f8e8d2178f47c6fa5209132e30d01b3e65b110b01fa02d74cd0d31f01208210f1381e5bba8e6a82101d0cfbd3ba8e5e6c44d3070101d4217f708e17511278f47c6fa53221995302baf2e06702a402de01b312e66c2120c200f2e06e23c200f2e06d5330bbf2e06d01f404217f708e17511278f47c6fa53221995302baf2e06702a402de01b312e66c2130d155239130e2e30d0c001030d307d402fb00d1019e3806d3ff0128b38e122084ffba923024965305baf2e3f0e205a405de01d2000101d3070101d32f0101d4d1239126912ae2523078f40e6fa1f2e3ef1ec705f2e3ef20f823bef2e06f20f823a1546d700e01d4f80703830cf94130038308f94130f8075006a18127f801a070f83681120670f836a0812bec70f836a0811d9870f836a022a60622a081053926a027a070f83823a481029827a070f838a003a60658a08106e05005a05005a0430370f83759a001a01cbef2e064f82850030f02b8017002c858cf160101cbffc98822c8cb01f400f400cb00c97021f90074c8cb0212ca07cbffc9d0c882109c73fba2580a02cb1fcb3f2601cb075250cc500b01cb2f1bcc2a01ca000a951901cb07089130e2102470408980188050db3c111000928e45c85801cb055005cf165003fa0254712323ed44ed45ed479f5bc85003cf17c913775003cb6bcccced67ed65ed64747fed11987601cb6bcc01cf17ed41edf101f2ffc901fb00db060842026305a8061c856c2ccf05dcb0df5815c71475870567cab5f049e340bcf59251f3ada4ac42"}
--------------------------------------------------------------------------------
/build/Order.compiled.json:
--------------------------------------------------------------------------------
1 | {"hex":"b5ee9c7241020c01000376000114ff00f4a413f4bcf2c80b01020162030200c7a1c771da89a1f48003f0c3a7fe03f0c441ae9380011c2c60dbf0c6dbf0c8dbf0cadbf0ccdbf0cedbf0d0dbf0d31c45a60e03f0c7a40003f0c9a803f0cba7fe03f0cda60e03f0cfa65e03f0d1a803f0d3a3c5f083f085f087f089f08bf08df08ff091f09303f8d03331d0d3030171b0915be0fa403001d31f01ed44d0fa4001f861d3ff01f86220d749c0008e16306df8636df8646df8656df8666df8676df8686df8698e22d30701f863d20001f864d401f865d3ff01f866d30701f867d32f01f868d401f869d1e220c000e30201d33f012282109c73fba2bae302028210a762230f070504014aba8e9bd3070101d1f845521078f40e6fa1f2e06a5230c705f2e06a59db3ce05f03840ff2f00802fe32f84113c705f2e068f8436e8ef101d30701f86370f864d401f86570f86670f867d32f01f868f848f823bef2e06fd401f869d200018e99d30701aef84621b0f2d06bf847a4f867f84601b1f86601db3c9131e2d1f849f846f845c8f841cf16f84201cbfff84301cb07f84401ca00cccbfff84701cb07f84801cb2fccc9ed540a06018ce001d30701f843baf2e069d401f900f845f900baf2e069d32f01f848baf2e069d401f900f849f900baf2e069d20001f2e069d3070101d1f845521078f40e6fa1f2e06a58db3c0801c83020d74ac0008e23c8708e1a22d7495230d71912cf1622d74a9402d74cd093317f58e2541220e63031c9d0df840f018b7617070726f76658c705f2f420707f8e19f84578f47c6fa5209b5243c70595317f327001de9132e201b3e632f2e06af82512db3c08026e8f335ced44ed45ed478e983170c88210afaf283e580402cb1fcb3fcb1f80108050db3ced67ed65ed64727fed118aed41edf101f2ffdb030b0902b4f844f2d07002aef84621b0f2d06bf847a4f867f84601b1f86670c8821082609bf62402cb1fcb3f80108050db3cdb3cf849f846f845c8f841cf16f84201cbfff84301cb07f84401ca00cccbfff84701cb07f84801cb2fccc9ed540b0a0180f847f843ba8eb6f84170f849c8821075097f5d580502cb1fcb3ff84201cbfff84801cb2ff84701cb07f845f90001cbff13cc128010810090db3c7ff8649130e20b00888e40c85801cb055004cf1658fa02547120ed44ed45ed479d5bc85003cf17c9127158cb6acced67ed65ed64737fed11977001cb6a01cf17ed41edf101f2ffc901fb00db0545f8021c"}
--------------------------------------------------------------------------------
/contracts/auto/order_code.func:
--------------------------------------------------------------------------------
1 |
2 | ;; https://docs.ton.org/tvm.pdf, page 30
3 | ;; Library reference cell — Always has level 0, and contains 8+256 data bits, including its 8-bit type integer 2
4 | ;; and the representation hash Hash(c) of the library cell being referred to. When loaded, a library
5 | ;; reference cell may be transparently replaced by the cell it refers to, if found in the current library context.
6 |
7 | cell order_code() asm "spec PUSHREF";
--------------------------------------------------------------------------------
/contracts/errors.func:
--------------------------------------------------------------------------------
1 | const int error::unauthorized_new_order = 1007;
2 | const int error::invalid_new_order = 1008;
3 | const int error::not_enough_ton = 100;
4 | const int error::unauthorized_execute = 101;
5 | const int error::singers_outdated = 102;
6 | const int error::invalid_dictionary_sequence = 103;
7 | const int error::unauthorized_init = 104;
8 | const int error::already_inited = 105;
9 | const int error::unauthorized_sign = 106;
10 | const int error::already_approved = 107;
11 | const int error::inconsistent_data = 108;
12 | const int error::invalid_threshold = 109;
13 | const int error::invalid_signers = 110;
14 | const int error::expired = 111;
15 | const int error::already_executed = 112;
16 |
17 | const int error::unknown_op = 0xffff;
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/contracts/helper/librarian.func:
--------------------------------------------------------------------------------
1 | ;; Simple library keeper
2 |
3 | #include "../imports/stdlib.fc";
4 | #include "../messages.func";
5 |
6 | const int DEFAULT_DURATION = 3600 * 24 * 365 * 10; ;; 10 years, can top-up in any time
7 | const int ONE_TON = 1000000000;
8 |
9 | ;; https://docs.ton.org/tvm.pdf, page 138, SETLIBCODE
10 | () set_lib_code(cell code, int mode) impure asm "SETLIBCODE";
11 |
12 | cell empty_cell() asm " PUSHREF";
13 |
14 | () recv_internal(int msg_value, cell in_msg_full, slice in_msg_body) impure {
15 | slice in_msg_full_slice = in_msg_full.begin_parse();
16 | int msg_flags = in_msg_full_slice~load_msg_flags();
17 | slice sender_address = in_msg_full_slice~load_msg_addr();
18 |
19 | cell lib_to_publish = get_data();
20 |
21 | int initial_gas = gas_consumed();
22 | (int order_cells, int order_bits, _) = compute_data_size(lib_to_publish, 1000); ;; according network config, max cells in library = 1000
23 | int size_counting_gas = gas_consumed() - initial_gas;
24 |
25 | int to_reserve = get_simple_compute_fee(MASTERCHAIN, size_counting_gas) +
26 | get_storage_fee(MASTERCHAIN, DEFAULT_DURATION, order_bits, order_cells);
27 | raw_reserve(to_reserve, RESERVE_BOUNCE_ON_ACTION_FAIL);
28 |
29 | send_message_with_only_body(sender_address, 0, begin_cell(), NON_BOUNCEABLE, SEND_MODE_CARRY_ALL_BALANCE);
30 | ;; https://docs.ton.org/tvm.pdf, page 138, SETLIBCODE
31 | set_lib_code(lib_to_publish, 2); ;; if x = 2, the library is added as a public library (and becomes available to all smart contracts if the current smart contract resides in the masterchain);
32 | ;; brick contract
33 | set_code(empty_cell());
34 | set_data(empty_cell());
35 | }
36 |
--------------------------------------------------------------------------------
/contracts/imports/stdlib.fc:
--------------------------------------------------------------------------------
1 | ;; Standard library for funC
2 | ;;
3 |
4 | {-
5 | This file is part of TON FunC Standard Library.
6 |
7 | FunC Standard Library is free software: you can redistribute it and/or modify
8 | it under the terms of the GNU Lesser General Public License as published by
9 | the Free Software Foundation, either version 2 of the License, or
10 | (at your option) any later version.
11 |
12 | FunC Standard Library is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | GNU Lesser General Public License for more details.
16 |
17 | -}
18 |
19 | {-
20 | # Tuple manipulation primitives
21 | The names and the types are mostly self-explaining.
22 | See [polymorhism with forall](https://ton.org/docs/#/func/functions?id=polymorphism-with-forall)
23 | for more info on the polymorphic functions.
24 |
25 | Note that currently values of atomic type `tuple` can't be cast to composite tuple type (e.g. `[int, cell]`)
26 | and vise versa.
27 | -}
28 |
29 | {-
30 | # Lisp-style lists
31 |
32 | Lists can be represented as nested 2-elements tuples.
33 | Empty list is conventionally represented as TVM `null` value (it can be obtained by calling [null()]).
34 | For example, tuple `(1, (2, (3, null)))` represents list `[1, 2, 3]`. Elements of a list can be of different types.
35 | -}
36 |
37 | ;;; Adds an element to the beginning of lisp-style list.
38 | forall X -> tuple cons(X head, tuple tail) asm "CONS";
39 |
40 | ;;; Extracts the head and the tail of lisp-style list.
41 | forall X -> (X, tuple) uncons(tuple list) asm "UNCONS";
42 |
43 | ;;; Extracts the tail and the head of lisp-style list.
44 | forall X -> (tuple, X) list_next(tuple list) asm(-> 1 0) "UNCONS";
45 |
46 | ;;; Returns the head of lisp-style list.
47 | forall X -> X car(tuple list) asm "CAR";
48 |
49 | ;;; Returns the tail of lisp-style list.
50 | tuple cdr(tuple list) asm "CDR";
51 |
52 | ;;; Creates tuple with zero elements.
53 | tuple empty_tuple() asm "NIL";
54 |
55 | ;;; Appends a value `x` to a `Tuple t = (x1, ..., xn)`, but only if the resulting `Tuple t' = (x1, ..., xn, x)`
56 | ;;; is of length at most 255. Otherwise throws a type check exception.
57 | forall X -> tuple tpush(tuple t, X value) asm "TPUSH";
58 | forall X -> (tuple, ()) ~tpush(tuple t, X value) asm "TPUSH";
59 |
60 | ;;; Creates a tuple of length one with given argument as element.
61 | forall X -> [X] single(X x) asm "SINGLE";
62 |
63 | ;;; Unpacks a tuple of length one
64 | forall X -> X unsingle([X] t) asm "UNSINGLE";
65 |
66 | ;;; Creates a tuple of length two with given arguments as elements.
67 | forall X, Y -> [X, Y] pair(X x, Y y) asm "PAIR";
68 |
69 | ;;; Unpacks a tuple of length two
70 | forall X, Y -> (X, Y) unpair([X, Y] t) asm "UNPAIR";
71 |
72 | ;;; Creates a tuple of length three with given arguments as elements.
73 | forall X, Y, Z -> [X, Y, Z] triple(X x, Y y, Z z) asm "TRIPLE";
74 |
75 | ;;; Unpacks a tuple of length three
76 | forall X, Y, Z -> (X, Y, Z) untriple([X, Y, Z] t) asm "UNTRIPLE";
77 |
78 | ;;; Creates a tuple of length four with given arguments as elements.
79 | forall X, Y, Z, W -> [X, Y, Z, W] tuple4(X x, Y y, Z z, W w) asm "4 TUPLE";
80 |
81 | ;;; Unpacks a tuple of length four
82 | forall X, Y, Z, W -> (X, Y, Z, W) untuple4([X, Y, Z, W] t) asm "4 UNTUPLE";
83 |
84 | ;;; Returns the first element of a tuple (with unknown element types).
85 | forall X -> X first(tuple t) asm "FIRST";
86 |
87 | ;;; Returns the second element of a tuple (with unknown element types).
88 | forall X -> X second(tuple t) asm "SECOND";
89 |
90 | ;;; Returns the third element of a tuple (with unknown element types).
91 | forall X -> X third(tuple t) asm "THIRD";
92 |
93 | ;;; Returns the fourth element of a tuple (with unknown element types).
94 | forall X -> X fourth(tuple t) asm "3 INDEX";
95 |
96 | ;;; Returns the first element of a pair tuple.
97 | forall X, Y -> X pair_first([X, Y] p) asm "FIRST";
98 |
99 | ;;; Returns the second element of a pair tuple.
100 | forall X, Y -> Y pair_second([X, Y] p) asm "SECOND";
101 |
102 | ;;; Returns the first element of a triple tuple.
103 | forall X, Y, Z -> X triple_first([X, Y, Z] p) asm "FIRST";
104 |
105 | ;;; Returns the second element of a triple tuple.
106 | forall X, Y, Z -> Y triple_second([X, Y, Z] p) asm "SECOND";
107 |
108 | ;;; Returns the third element of a triple tuple.
109 | forall X, Y, Z -> Z triple_third([X, Y, Z] p) asm "THIRD";
110 |
111 |
112 | ;;; Push null element (casted to given type)
113 | ;;; By the TVM type `Null` FunC represents absence of a value of some atomic type.
114 | ;;; So `null` can actually have any atomic type.
115 | forall X -> X null() asm "PUSHNULL";
116 |
117 | ;;; Moves a variable [x] to the top of the stack
118 | forall X -> (X, ()) ~impure_touch(X x) impure asm "NOP";
119 |
120 |
121 |
122 | ;;; Returns the current Unix time as an Integer
123 | int now() asm "NOW";
124 |
125 | ;;; Returns the internal address of the current smart contract as a Slice with a `MsgAddressInt`.
126 | ;;; If necessary, it can be parsed further using primitives such as [parse_std_addr].
127 | slice my_address() asm "MYADDR";
128 |
129 | ;;; Returns the balance of the smart contract as a tuple consisting of an int
130 | ;;; (balance in nanotoncoins) and a `cell`
131 | ;;; (a dictionary with 32-bit keys representing the balance of "extra currencies")
132 | ;;; at the start of Computation Phase.
133 | ;;; Note that RAW primitives such as [send_raw_message] do not update this field.
134 | [int, cell] get_balance() asm "BALANCE";
135 |
136 | ;;; Returns the logical time of the current transaction.
137 | int cur_lt() asm "LTIME";
138 |
139 | ;;; Returns the starting logical time of the current block.
140 | int block_lt() asm "BLOCKLT";
141 |
142 | ;;; Computes the representation hash of a `cell` [c] and returns it as a 256-bit unsigned integer `x`.
143 | ;;; Useful for signing and checking signatures of arbitrary entities represented by a tree of cells.
144 | int cell_hash(cell c) asm "HASHCU";
145 |
146 | ;;; Computes the hash of a `slice s` and returns it as a 256-bit unsigned integer `x`.
147 | ;;; The result is the same as if an ordinary cell containing only data and references from `s` had been created
148 | ;;; and its hash computed by [cell_hash].
149 | int slice_hash(slice s) asm "HASHSU";
150 |
151 | ;;; Computes sha256 of the data bits of `slice` [s]. If the bit length of `s` is not divisible by eight,
152 | ;;; throws a cell underflow exception. The hash value is returned as a 256-bit unsigned integer `x`.
153 | int string_hash(slice s) asm "SHA256U";
154 |
155 | {-
156 | # Signature checks
157 | -}
158 |
159 | ;;; Checks the Ed25519-`signature` of a `hash` (a 256-bit unsigned integer, usually computed as the hash of some data)
160 | ;;; using [public_key] (also represented by a 256-bit unsigned integer).
161 | ;;; The signature must contain at least 512 data bits; only the first 512 bits are used.
162 | ;;; The result is `−1` if the signature is valid, `0` otherwise.
163 | ;;; Note that `CHKSIGNU` creates a 256-bit slice with the hash and calls `CHKSIGNS`.
164 | ;;; That is, if [hash] is computed as the hash of some data, these data are hashed twice,
165 | ;;; the second hashing occurring inside `CHKSIGNS`.
166 | int check_signature(int hash, slice signature, int public_key) asm "CHKSIGNU";
167 |
168 | ;;; Checks whether [signature] is a valid Ed25519-signature of the data portion of `slice data` using `public_key`,
169 | ;;; similarly to [check_signature].
170 | ;;; If the bit length of [data] is not divisible by eight, throws a cell underflow exception.
171 | ;;; The verification of Ed25519 signatures is the standard one,
172 | ;;; with sha256 used to reduce [data] to the 256-bit number that is actually signed.
173 | int check_data_signature(slice data, slice signature, int public_key) asm "CHKSIGNS";
174 |
175 | {---
176 | # Computation of boc size
177 | The primitives below may be useful for computing storage fees of user-provided data.
178 | -}
179 |
180 | ;;; Returns `(x, y, z, -1)` or `(null, null, null, 0)`.
181 | ;;; Recursively computes the count of distinct cells `x`, data bits `y`, and cell references `z`
182 | ;;; in the DAG rooted at `cell` [c], effectively returning the total storage used by this DAG taking into account
183 | ;;; the identification of equal cells.
184 | ;;; The values of `x`, `y`, and `z` are computed by a depth-first traversal of this DAG,
185 | ;;; with a hash table of visited cell hashes used to prevent visits of already-visited cells.
186 | ;;; The total count of visited cells `x` cannot exceed non-negative [max_cells];
187 | ;;; otherwise the computation is aborted before visiting the `(max_cells + 1)`-st cell and
188 | ;;; a zero flag is returned to indicate failure. If [c] is `null`, returns `x = y = z = 0`.
189 | (int, int, int) compute_data_size(cell c, int max_cells) impure asm "CDATASIZE";
190 |
191 | ;;; Similar to [compute_data_size?], but accepting a `slice` [s] instead of a `cell`.
192 | ;;; The returned value of `x` does not take into account the cell that contains the `slice` [s] itself;
193 | ;;; however, the data bits and the cell references of [s] are accounted for in `y` and `z`.
194 | (int, int, int) slice_compute_data_size(slice s, int max_cells) impure asm "SDATASIZE";
195 |
196 | ;;; A non-quiet version of [compute_data_size?] that throws a cell overflow exception (`8`) on failure.
197 | (int, int, int, int) compute_data_size?(cell c, int max_cells) asm "CDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT";
198 |
199 | ;;; A non-quiet version of [slice_compute_data_size?] that throws a cell overflow exception (8) on failure.
200 | (int, int, int, int) slice_compute_data_size?(cell c, int max_cells) asm "SDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT";
201 |
202 | ;;; Throws an exception with exit_code excno if cond is not 0 (commented since implemented in compilator)
203 | ;; () throw_if(int excno, int cond) impure asm "THROWARGIF";
204 |
205 | {--
206 | # Debug primitives
207 | Only works for local TVM execution with debug level verbosity
208 | -}
209 | ;;; Dumps the stack (at most the top 255 values) and shows the total stack depth.
210 | () dump_stack() impure asm "DUMPSTK";
211 |
212 | {-
213 | # Persistent storage save and load
214 | -}
215 |
216 | ;;; Returns the persistent contract storage cell. It can be parsed or modified with slice and builder primitives later.
217 | cell get_data() asm "c4 PUSH";
218 |
219 | ;;; Sets `cell` [c] as persistent contract data. You can update persistent contract storage with this primitive.
220 | () set_data(cell c) impure asm "c4 POP";
221 |
222 | {-
223 | # Continuation primitives
224 | -}
225 | ;;; Usually `c3` has a continuation initialized by the whole code of the contract. It is used for function calls.
226 | ;;; The primitive returns the current value of `c3`.
227 | cont get_c3() impure asm "c3 PUSH";
228 |
229 | ;;; Updates the current value of `c3`. Usually, it is used for updating smart contract code in run-time.
230 | ;;; Note that after execution of this primitive the current code
231 | ;;; (and the stack of recursive function calls) won't change,
232 | ;;; but any other function call will use a function from the new code.
233 | () set_c3(cont c) impure asm "c3 POP";
234 |
235 | ;;; Transforms a `slice` [s] into a simple ordinary continuation `c`, with `c.code = s` and an empty stack and savelist.
236 | cont bless(slice s) impure asm "BLESS";
237 |
238 | {---
239 | # Gas related primitives
240 | -}
241 |
242 | ;;; Sets current gas limit `gl` to its maximal allowed value `gm`, and resets the gas credit `gc` to zero,
243 | ;;; decreasing the value of `gr` by `gc` in the process.
244 | ;;; In other words, the current smart contract agrees to buy some gas to finish the current transaction.
245 | ;;; This action is required to process external messages, which bring no value (hence no gas) with themselves.
246 | ;;;
247 | ;;; For more details check [accept_message effects](https://ton.org/docs/#/smart-contracts/accept).
248 | () accept_message() impure asm "ACCEPT";
249 |
250 | ;;; Sets current gas limit `gl` to the minimum of limit and `gm`, and resets the gas credit `gc` to zero.
251 | ;;; If the gas consumed so far (including the present instruction) exceeds the resulting value of `gl`,
252 | ;;; an (unhandled) out of gas exception is thrown before setting new gas limits.
253 | ;;; Notice that [set_gas_limit] with an argument `limit ≥ 2^63 − 1` is equivalent to [accept_message].
254 | () set_gas_limit(int limit) impure asm "SETGASLIMIT";
255 |
256 | ;;; Commits the current state of registers `c4` (“persistent data”) and `c5` (“actions”)
257 | ;;; so that the current execution is considered “successful” with the saved values even if an exception
258 | ;;; in Computation Phase is thrown later.
259 | () commit() impure asm "COMMIT";
260 |
261 | ;;; Not implemented
262 | ;;; Computes the amount of gas that can be bought for `amount` nanoTONs,
263 | ;;; and sets `gl` accordingly in the same way as [set_gas_limit].
264 | ;;() buy_gas(int amount) impure asm "BUYGAS";
265 |
266 | ;;; Computes the minimum of two integers [x] and [y].
267 | int min(int x, int y) asm "MIN";
268 |
269 | ;;; Computes the maximum of two integers [x] and [y].
270 | int max(int x, int y) asm "MAX";
271 |
272 | ;;; Sorts two integers.
273 | (int, int) minmax(int x, int y) asm "MINMAX";
274 |
275 | ;;; Computes the absolute value of an integer [x].
276 | int abs(int x) asm "ABS";
277 |
278 | {-
279 | # Slice primitives
280 |
281 | It is said that a primitive _loads_ some data,
282 | if it returns the data and the remainder of the slice
283 | (so it can also be used as [modifying method](https://ton.org/docs/#/func/statements?id=modifying-methods)).
284 |
285 | It is said that a primitive _preloads_ some data, if it returns only the data
286 | (it can be used as [non-modifying method](https://ton.org/docs/#/func/statements?id=non-modifying-methods)).
287 |
288 | Unless otherwise stated, loading and preloading primitives read the data from a prefix of the slice.
289 | -}
290 |
291 |
292 | ;;; Converts a `cell` [c] into a `slice`. Notice that [c] must be either an ordinary cell,
293 | ;;; or an exotic cell (see [TVM.pdf](https://ton-blockchain.github.io/docs/tvm.pdf), 3.1.2)
294 | ;;; which is automatically loaded to yield an ordinary cell `c'`, converted into a `slice` afterwards.
295 | slice begin_parse(cell c) asm "CTOS";
296 |
297 | ;;; Checks if [s] is empty. If not, throws an exception.
298 | () end_parse(slice s) impure asm "ENDS";
299 |
300 | ;;; Loads the first reference from the slice.
301 | (slice, cell) load_ref(slice s) asm(-> 1 0) "LDREF";
302 |
303 | ;;; Preloads the first reference from the slice.
304 | cell preload_ref(slice s) asm "PLDREF";
305 |
306 | {- Functions below are commented because are implemented on compilator level for optimisation -}
307 |
308 | ;;; Loads a signed [len]-bit integer from a slice [s].
309 | ;; (slice, int) ~load_int(slice s, int len) asm(s len -> 1 0) "LDIX";
310 |
311 | ;;; Loads an unsigned [len]-bit integer from a slice [s].
312 | ;; (slice, int) ~load_uint(slice s, int len) asm( -> 1 0) "LDUX";
313 |
314 | ;;; Preloads a signed [len]-bit integer from a slice [s].
315 | ;; int preload_int(slice s, int len) asm "PLDIX";
316 |
317 | ;;; Preloads an unsigned [len]-bit integer from a slice [s].
318 | ;; int preload_uint(slice s, int len) asm "PLDUX";
319 |
320 | ;;; Loads the first `0 ≤ len ≤ 1023` bits from slice [s] into a separate `slice s''`.
321 | ;; (slice, slice) load_bits(slice s, int len) asm(s len -> 1 0) "LDSLICEX";
322 |
323 | ;;; Preloads the first `0 ≤ len ≤ 1023` bits from slice [s] into a separate `slice s''`.
324 | ;; slice preload_bits(slice s, int len) asm "PLDSLICEX";
325 |
326 | ;;; Loads serialized amount of TonCoins (any unsigned integer up to `2^128 - 1`).
327 | (slice, int) load_grams(slice s) asm(-> 1 0) "LDGRAMS";
328 | (slice, int) load_coins(slice s) asm(-> 1 0) "LDVARUINT16";
329 |
330 | ;;; Returns all but the first `0 ≤ len ≤ 1023` bits of `slice` [s].
331 | slice skip_bits(slice s, int len) asm "SDSKIPFIRST";
332 | (slice, ()) ~skip_bits(slice s, int len) asm "SDSKIPFIRST";
333 |
334 | ;;; Returns the first `0 ≤ len ≤ 1023` bits of `slice` [s].
335 | slice first_bits(slice s, int len) asm "SDCUTFIRST";
336 |
337 | ;;; Returns all but the last `0 ≤ len ≤ 1023` bits of `slice` [s].
338 | slice skip_last_bits(slice s, int len) asm "SDSKIPLAST";
339 | (slice, ()) ~skip_last_bits(slice s, int len) asm "SDSKIPLAST";
340 |
341 | ;;; Returns the last `0 ≤ len ≤ 1023` bits of `slice` [s].
342 | slice slice_last(slice s, int len) asm "SDCUTLAST";
343 |
344 | ;;; Loads a dictionary `D` (HashMapE) from `slice` [s].
345 | ;;; (returns `null` if `nothing` constructor is used).
346 | (slice, cell) load_dict(slice s) asm(-> 1 0) "LDDICT";
347 |
348 | ;;; Preloads a dictionary `D` from `slice` [s].
349 | cell preload_dict(slice s) asm "PLDDICT";
350 |
351 | ;;; Loads a dictionary as [load_dict], but returns only the remainder of the slice.
352 | slice skip_dict(slice s) asm "SKIPDICT";
353 | (slice, ()) ~skip_dict(slice s) asm "SKIPDICT";
354 |
355 | ;;; Loads (Maybe ^Cell) from `slice` [s].
356 | ;;; In other words loads 1 bit and if it is true
357 | ;;; loads first ref and return it with slice remainder
358 | ;;; otherwise returns `null` and slice remainder
359 | (slice, cell) load_maybe_ref(slice s) asm(-> 1 0) "LDOPTREF";
360 |
361 | ;;; Preloads (Maybe ^Cell) from `slice` [s].
362 | cell preload_maybe_ref(slice s) asm "PLDOPTREF";
363 |
364 |
365 | ;;; Returns the depth of `cell` [c].
366 | ;;; If [c] has no references, then return `0`;
367 | ;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [c].
368 | ;;; If [c] is a `null` instead of a cell, returns zero.
369 | int cell_depth(cell c) asm "CDEPTH";
370 |
371 |
372 | {-
373 | # Slice size primitives
374 | -}
375 |
376 | ;;; Returns the number of references in `slice` [s].
377 | int slice_refs(slice s) asm "SREFS";
378 |
379 | ;;; Returns the number of data bits in `slice` [s].
380 | int slice_bits(slice s) asm "SBITS";
381 |
382 | ;;; Returns both the number of data bits and the number of references in `slice` [s].
383 | (int, int) slice_bits_refs(slice s) asm "SBITREFS";
384 |
385 | ;;; Checks whether a `slice` [s] is empty (i.e., contains no bits of data and no cell references).
386 | int slice_empty?(slice s) asm "SEMPTY";
387 |
388 | ;;; Checks whether `slice` [s] has no bits of data.
389 | int slice_data_empty?(slice s) asm "SDEMPTY";
390 |
391 | ;;; Checks whether `slice` [s] has no references.
392 | int slice_refs_empty?(slice s) asm "SREMPTY";
393 |
394 | ;;; Returns the depth of `slice` [s].
395 | ;;; If [s] has no references, then returns `0`;
396 | ;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [s].
397 | int slice_depth(slice s) asm "SDEPTH";
398 |
399 | {-
400 | # Builder size primitives
401 | -}
402 |
403 | ;;; Returns the number of cell references already stored in `builder` [b]
404 | int builder_refs(builder b) asm "BREFS";
405 |
406 | ;;; Returns the number of data bits already stored in `builder` [b].
407 | int builder_bits(builder b) asm "BBITS";
408 |
409 | ;;; Returns the depth of `builder` [b].
410 | ;;; If no cell references are stored in [b], then returns 0;
411 | ;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [b].
412 | int builder_depth(builder b) asm "BDEPTH";
413 |
414 | {-
415 | # Builder primitives
416 | It is said that a primitive _stores_ a value `x` into a builder `b`
417 | if it returns a modified version of the builder `b'` with the value `x` stored at the end of it.
418 | It can be used as [non-modifying method](https://ton.org/docs/#/func/statements?id=non-modifying-methods).
419 |
420 | All the primitives below first check whether there is enough space in the `builder`,
421 | and only then check the range of the value being serialized.
422 | -}
423 |
424 | ;;; Creates a new empty `builder`.
425 | builder begin_cell() asm "NEWC";
426 |
427 | ;;; Converts a `builder` into an ordinary `cell`.
428 | cell end_cell(builder b) asm "ENDC";
429 |
430 | ;;; Stores a reference to `cell` [c] into `builder` [b].
431 | builder store_ref(builder b, cell c) asm(c b) "STREF";
432 |
433 | ;;; Stores an unsigned [len]-bit integer `x` into `b` for `0 ≤ len ≤ 256`.
434 | ;; builder store_uint(builder b, int x, int len) asm(x b len) "STUX";
435 |
436 | ;;; Stores a signed [len]-bit integer `x` into `b` for` 0 ≤ len ≤ 257`.
437 | ;; builder store_int(builder b, int x, int len) asm(x b len) "STIX";
438 |
439 |
440 | ;;; Stores `slice` [s] into `builder` [b]
441 | builder store_slice(builder b, slice s) asm "STSLICER";
442 |
443 | ;;; Stores (serializes) an integer [x] in the range `0..2^128 − 1` into `builder` [b].
444 | ;;; The serialization of [x] consists of a 4-bit unsigned big-endian integer `l`,
445 | ;;; which is the smallest integer `l ≥ 0`, such that `x < 2^8l`,
446 | ;;; followed by an `8l`-bit unsigned big-endian representation of [x].
447 | ;;; If [x] does not belong to the supported range, a range check exception is thrown.
448 | ;;;
449 | ;;; Store amounts of TonCoins to the builder as VarUInteger 16
450 | builder store_grams(builder b, int x) asm "STGRAMS";
451 | builder store_coins(builder b, int x) asm "STVARUINT16";
452 |
453 | ;;; Stores dictionary `D` represented by `cell` [c] or `null` into `builder` [b].
454 | ;;; In other words, stores a `1`-bit and a reference to [c] if [c] is not `null` and `0`-bit otherwise.
455 | builder store_dict(builder b, cell c) asm(c b) "STDICT";
456 |
457 | ;;; Stores (Maybe ^Cell) to builder:
458 | ;;; if cell is null store 1 zero bit
459 | ;;; otherwise store 1 true bit and ref to cell
460 | builder store_maybe_ref(builder b, cell c) asm(c b) "STOPTREF";
461 |
462 |
463 | {-
464 | # Address manipulation primitives
465 | The address manipulation primitives listed below serialize and deserialize values according to the following TL-B scheme:
466 | ```TL-B
467 | addr_none$00 = MsgAddressExt;
468 | addr_extern$01 len:(## 8) external_address:(bits len)
469 | = MsgAddressExt;
470 | anycast_info$_ depth:(#<= 30) { depth >= 1 }
471 | rewrite_pfx:(bits depth) = Anycast;
472 | addr_std$10 anycast:(Maybe Anycast)
473 | workchain_id:int8 address:bits256 = MsgAddressInt;
474 | addr_var$11 anycast:(Maybe Anycast) addr_len:(## 9)
475 | workchain_id:int32 address:(bits addr_len) = MsgAddressInt;
476 | _ _:MsgAddressInt = MsgAddress;
477 | _ _:MsgAddressExt = MsgAddress;
478 |
479 | int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
480 | src:MsgAddress dest:MsgAddressInt
481 | value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams
482 | created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed;
483 | ext_out_msg_info$11 src:MsgAddress dest:MsgAddressExt
484 | created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed;
485 | ```
486 | A deserialized `MsgAddress` is represented by a tuple `t` as follows:
487 |
488 | - `addr_none` is represented by `t = (0)`,
489 | i.e., a tuple containing exactly one integer equal to zero.
490 | - `addr_extern` is represented by `t = (1, s)`,
491 | where slice `s` contains the field `external_address`. In other words, `
492 | t` is a pair (a tuple consisting of two entries), containing an integer equal to one and slice `s`.
493 | - `addr_std` is represented by `t = (2, u, x, s)`,
494 | where `u` is either a `null` (if `anycast` is absent) or a slice `s'` containing `rewrite_pfx` (if anycast is present).
495 | Next, integer `x` is the `workchain_id`, and slice `s` contains the address.
496 | - `addr_var` is represented by `t = (3, u, x, s)`,
497 | where `u`, `x`, and `s` have the same meaning as for `addr_std`.
498 | -}
499 |
500 | ;;; Loads from slice [s] the only prefix that is a valid `MsgAddress`,
501 | ;;; and returns both this prefix `s'` and the remainder `s''` of [s] as slices.
502 | (slice, slice) load_msg_addr(slice s) asm(-> 1 0) "LDMSGADDR";
503 |
504 | ;;; Decomposes slice [s] containing a valid `MsgAddress` into a `tuple t` with separate fields of this `MsgAddress`.
505 | ;;; If [s] is not a valid `MsgAddress`, a cell deserialization exception is thrown.
506 | tuple parse_addr(slice s) asm "PARSEMSGADDR";
507 |
508 | ;;; Parses slice [s] containing a valid `MsgAddressInt` (usually a `msg_addr_std`),
509 | ;;; applies rewriting from the anycast (if present) to the same-length prefix of the address,
510 | ;;; and returns both the workchain and the 256-bit address as integers.
511 | ;;; If the address is not 256-bit, or if [s] is not a valid serialization of `MsgAddressInt`,
512 | ;;; throws a cell deserialization exception.
513 | (int, int) parse_std_addr(slice s) asm "REWRITESTDADDR";
514 |
515 | ;;; A variant of [parse_std_addr] that returns the (rewritten) address as a slice [s],
516 | ;;; even if it is not exactly 256 bit long (represented by a `msg_addr_var`).
517 | (int, slice) parse_var_addr(slice s) asm "REWRITEVARADDR";
518 |
519 | {-
520 | # Dictionary primitives
521 | -}
522 |
523 |
524 | ;;; Sets the value associated with [key_len]-bit key signed index in dictionary [dict] to [value] (cell),
525 | ;;; and returns the resulting dictionary.
526 | cell idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF";
527 | (cell, ()) ~idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF";
528 |
529 | ;;; Sets the value associated with [key_len]-bit key unsigned index in dictionary [dict] to [value] (cell),
530 | ;;; and returns the resulting dictionary.
531 | cell udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF";
532 | (cell, ()) ~udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF";
533 |
534 | cell idict_get_ref(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETOPTREF";
535 | (cell, int) idict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETREF" "NULLSWAPIFNOT";
536 | (cell, int) udict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETREF" "NULLSWAPIFNOT";
537 | (cell, cell) idict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETGETOPTREF";
538 | (cell, cell) udict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETGETOPTREF";
539 | (cell, int) idict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDEL";
540 | (cell, int) udict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDEL";
541 | (slice, int) idict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGET" "NULLSWAPIFNOT";
542 | (slice, int) udict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGET" "NULLSWAPIFNOT";
543 | (cell, slice, int) idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT";
544 | (cell, slice, int) udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT";
545 | (cell, (slice, int)) ~idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT";
546 | (cell, (slice, int)) ~udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT";
547 | cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET";
548 | (cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET";
549 | cell idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET";
550 | (cell, ()) ~idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET";
551 | cell dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET";
552 | (cell, ()) ~dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET";
553 | (cell, int) udict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUADD";
554 | (cell, int) udict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUREPLACE";
555 | (cell, int) idict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIADD";
556 | (cell, int) idict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIREPLACE";
557 | cell udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB";
558 | (cell, ()) ~udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB";
559 | cell idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB";
560 | (cell, ()) ~idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB";
561 | cell dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB";
562 | (cell, ()) ~dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB";
563 | (cell, int) udict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUADDB";
564 | (cell, int) udict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUREPLACEB";
565 | (cell, int) idict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIADDB";
566 | (cell, int) idict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIREPLACEB";
567 | (cell, int, slice, int) udict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2";
568 | (cell, (int, slice, int)) ~udict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2";
569 | (cell, int, slice, int) idict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2";
570 | (cell, (int, slice, int)) ~idict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2";
571 | (cell, slice, slice, int) dict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2";
572 | (cell, (slice, slice, int)) ~dict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2";
573 | (cell, int, slice, int) udict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2";
574 | (cell, (int, slice, int)) ~udict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2";
575 | (cell, int, slice, int) idict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2";
576 | (cell, (int, slice, int)) ~idict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2";
577 | (cell, slice, slice, int) dict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2";
578 | (cell, (slice, slice, int)) ~dict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2";
579 | (int, slice, int) udict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMIN" "NULLSWAPIFNOT2";
580 | (int, slice, int) udict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAX" "NULLSWAPIFNOT2";
581 | (int, cell, int) udict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMINREF" "NULLSWAPIFNOT2";
582 | (int, cell, int) udict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAXREF" "NULLSWAPIFNOT2";
583 | (int, slice, int) idict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMIN" "NULLSWAPIFNOT2";
584 | (int, slice, int) idict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAX" "NULLSWAPIFNOT2";
585 | (int, cell, int) idict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMINREF" "NULLSWAPIFNOT2";
586 | (int, cell, int) idict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAXREF" "NULLSWAPIFNOT2";
587 | (int, slice, int) udict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXT" "NULLSWAPIFNOT2";
588 | (int, slice, int) udict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXTEQ" "NULLSWAPIFNOT2";
589 | (int, slice, int) udict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREV" "NULLSWAPIFNOT2";
590 | (int, slice, int) udict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREVEQ" "NULLSWAPIFNOT2";
591 | (int, slice, int) idict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXT" "NULLSWAPIFNOT2";
592 | (int, slice, int) idict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXTEQ" "NULLSWAPIFNOT2";
593 | (int, slice, int) idict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREV" "NULLSWAPIFNOT2";
594 | (int, slice, int) idict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREVEQ" "NULLSWAPIFNOT2";
595 |
596 | ;;; Creates an empty dictionary, which is actually a null value. Equivalent to PUSHNULL
597 | cell new_dict() asm "NEWDICT";
598 | ;;; Checks whether a dictionary is empty. Equivalent to cell_null?.
599 | int dict_empty?(cell c) asm "DICTEMPTY";
600 |
601 |
602 | {- Prefix dictionary primitives -}
603 | (slice, slice, slice, int) pfxdict_get?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTGETQ" "NULLSWAPIFNOT2";
604 | (cell, int) pfxdict_set?(cell dict, int key_len, slice key, slice value) asm(value key dict key_len) "PFXDICTSET";
605 | (cell, int) pfxdict_delete?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTDEL";
606 |
607 | ;;; Returns the value of the global configuration parameter with integer index `i` as a `cell` or `null` value.
608 | cell config_param(int x) asm "CONFIGOPTPARAM";
609 | ;;; Checks whether c is a null. Note, that FunC also has polymorphic null? built-in.
610 | int cell_null?(cell c) asm "ISNULL";
611 |
612 | ;;; 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.
613 | () raw_reserve(int amount, int mode) impure asm "RAWRESERVE";
614 | ;;; 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.
615 | () raw_reserve_extra(int amount, cell extra_amount, int mode) impure asm "RAWRESERVEX";
616 | ;;; 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.
617 | () send_raw_message(cell msg, int mode) impure asm "SENDRAWMSG";
618 | ;;; 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
619 | () set_code(cell new_code) impure asm "SETCODE";
620 |
621 | ;;; 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.
622 | int random() impure asm "RANDU256";
623 | ;;; 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.
624 | int rand(int range) impure asm "RAND";
625 | ;;; Returns the current random seed as an unsigned 256-bit Integer.
626 | int get_seed() impure asm "RANDSEED";
627 | ;;; Sets the random seed to unsigned 256-bit seed.
628 | () set_seed(int x) impure asm "SETRAND";
629 | ;;; 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.
630 | () randomize(int x) impure asm "ADDRAND";
631 | ;;; Equivalent to randomize(cur_lt());.
632 | () randomize_lt() impure asm "LTIME" "ADDRAND";
633 |
634 | ;;; Checks whether the data parts of two slices coinside
635 | int equal_slices_bits(slice a, slice b) asm "SDEQ";
636 | ;;; Checks whether b is a null. Note, that FunC also has polymorphic null? built-in.
637 | int builder_null?(builder b) asm "ISNULL";
638 | ;;; Concatenates two builders
639 | builder store_builder(builder to, builder from) asm "STBR";
640 |
641 | ;; CUSTOM:
642 |
643 | ;; TVM UPGRADE 2023-07 https://docs.ton.org/learn/tvm-instructions/tvm-upgrade-2023-07
644 | ;; In mainnet since 20 Dec 2023 https://t.me/tonblockchain/226
645 |
646 | ;;; Retrieves code of smart-contract from c7
647 | cell my_code() asm "MYCODE";
648 |
649 | ;;; Creates an output action and returns a fee for creating a message. Mode has the same effect as in the case of SENDRAWMSG
650 | int send_message(cell msg, int mode) impure asm "SENDMSG";
651 |
652 | int gas_consumed() asm "GASCONSUMED";
653 |
654 | ;; TVM V6 https://github.com/ton-blockchain/ton/blob/testnet/doc/GlobalVersions.md#version-6
655 |
656 | int get_compute_fee(int workchain, int gas_used) asm(gas_used workchain) "GETGASFEE";
657 | int get_storage_fee(int workchain, int seconds, int bits, int cells) asm(cells bits seconds workchain) "GETSTORAGEFEE";
658 | int get_forward_fee(int workchain, int bits, int cells) asm(cells bits workchain) "GETFORWARDFEE";
659 | int get_precompiled_gas_consumption() asm "GETPRECOMPILEDGAS";
660 |
661 | int get_simple_compute_fee(int workchain, int gas_used) asm(gas_used workchain) "GETGASFEESIMPLE";
662 | int get_simple_forward_fee(int workchain, int bits, int cells) asm(cells bits workchain) "GETFORWARDFEESIMPLE";
663 | int get_original_fwd_fee(int workchain, int fwd_fee) asm(fwd_fee workchain) "GETORIGINALFWDFEE";
664 | int my_storage_due() asm "DUEPAYMENT";
665 |
666 | tuple get_fee_cofigs() asm "UNPACKEDCONFIGTUPLE";
667 |
668 | ;; BASIC
669 |
670 | const int TRUE = -1;
671 | const int FALSE = 0;
672 |
673 | const int MASTERCHAIN = -1;
674 | const int BASECHAIN = 0;
675 |
676 | const int CELL_BITS = 1023;
677 | const int CELL_REFS = 4;
678 |
679 | ;;; skip (Maybe ^Cell) from `slice` [s].
680 | (slice, ()) ~skip_maybe_ref(slice s) asm "SKIPOPTREF";
681 |
682 | (slice, int) ~load_bool(slice s) inline {
683 | return s.load_int(1);
684 | }
685 |
686 | builder store_bool(builder b, int value) inline {
687 | return b.store_int(value, 1);
688 | }
689 |
690 | ;; ADDRESS NONE
691 | ;; addr_none$00 = MsgAddressExt; https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L100
692 |
693 | builder store_address_none(builder b) inline {
694 | return b.store_uint(0, 2);
695 | }
696 |
697 | slice address_none() asm " = 1);
42 | throw_unless(error::invalid_threshold, threshold > 0);
43 | throw_unless(error::invalid_threshold, threshold <= signers_num);
44 |
45 | proposers = action~load_dict();
46 | validate_dictionary_sequence(proposers);
47 |
48 | action.end_parse();
49 | }
50 | }
51 | } until (~ found?);
52 |
53 | return ((threshold, signers, signers_num, proposers), ());
54 | }
55 |
56 |
57 | (int, int, cell, int, cell, int) load_data() inline {
58 | slice ds = get_data().begin_parse();
59 | var data = (
60 | ds~load_order_seqno(), ;; next_order_seqno
61 | ds~load_index(), ;; threshold
62 | ds~load_nonempty_dict(), ;; signers
63 | ds~load_index(), ;; signers_num
64 | ds~load_dict(), ;; proposers
65 | ds~load_bool() ;; allow_arbitrary_order_seqno
66 | );
67 | ds.end_parse();
68 | return data;
69 | }
70 |
71 | () save_data(int next_order_seqno, int threshold, cell signers, int signers_num, cell proposers, int allow_arbitrary_order_seqno) impure inline {
72 | set_data(
73 | begin_cell()
74 | .store_order_seqno(next_order_seqno)
75 | .store_index(threshold)
76 | .store_nonempty_dict(signers)
77 | .store_index(signers_num)
78 | .store_dict(proposers)
79 | .store_bool(allow_arbitrary_order_seqno)
80 | .end_cell()
81 | );
82 | }
83 |
84 | () recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) {
85 | slice in_msg_full_slice = in_msg_full.begin_parse();
86 | int msg_flags = in_msg_full_slice~load_msg_flags();
87 | if (msg_flags & 1) { ;; is bounced
88 | return ();
89 | }
90 | slice sender_address = in_msg_full_slice~load_msg_addr();
91 |
92 | if (in_msg_body.slice_bits() == 0) {
93 | return (); ;; empty message - just accept TONs
94 | }
95 |
96 | int op = in_msg_body~load_op();
97 |
98 | if (op == 0) {
99 | return (); ;; simple text message - just accept TONs
100 | }
101 |
102 | int query_id = in_msg_body~load_query_id();
103 |
104 | (int next_order_seqno, int threshold, cell signers, int signers_num, cell proposers, int allow_arbitrary_order_seqno) = load_data();
105 |
106 | if (op == op::new_order) {
107 | int order_seqno = in_msg_body~load_order_seqno();
108 | if (~ allow_arbitrary_order_seqno) {
109 | if (order_seqno == MAX_ORDER_SEQNO) {
110 | order_seqno = next_order_seqno;
111 | } else {
112 | throw_unless(error::invalid_new_order, (order_seqno == next_order_seqno));
113 | }
114 | next_order_seqno += 1;
115 | }
116 |
117 | int signer? = in_msg_body~load_bool();
118 | int index = in_msg_body~load_index();
119 | int expiration_date = in_msg_body~load_timestamp();
120 | cell order_body = in_msg_body~load_ref();
121 | in_msg_body.end_parse();
122 | (slice expected_address, int found?) = (signer? ? signers : proposers).udict_get?(INDEX_SIZE, index);
123 | throw_unless(error::unauthorized_new_order, found?);
124 | throw_unless(error::unauthorized_new_order, equal_slices_bits(sender_address, expected_address));
125 | throw_unless(error::expired, expiration_date >= now());
126 |
127 | int minimal_value = calculate_order_processing_cost(order_body, signers, expiration_date - now());
128 | throw_unless(error::not_enough_ton, msg_value >= minimal_value);
129 |
130 | cell state_init = calculate_order_state_init(my_address(), order_seqno);
131 | slice order_address = calculate_address_by_state_init(BASECHAIN, state_init);
132 | builder init_body = begin_cell()
133 | .store_op_and_query_id(op::init, query_id)
134 | .store_index(threshold)
135 | .store_nonempty_dict(signers)
136 | .store_timestamp(expiration_date)
137 | .store_ref(order_body)
138 | .store_bool(signer?);
139 | if (signer?) {
140 | init_body = init_body.store_index(index);
141 | }
142 | send_message_with_state_init_and_body(
143 | order_address,
144 | 0,
145 | state_init,
146 | init_body,
147 | BOUNCEABLE,
148 | SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE | SEND_MODE_BOUNCE_ON_ACTION_FAIL
149 | );
150 |
151 | } elseif (op == op::execute) {
152 | ;; check that sender is order smart-contract and check that it has recent
153 | ;; signers dict
154 |
155 | int order_seqno = in_msg_body~load_order_seqno();
156 | int expiration_date = in_msg_body~load_timestamp();
157 | int approvals_num = in_msg_body~load_index();
158 | int signers_hash = in_msg_body~load_hash();
159 | cell order_body = in_msg_body~load_ref();
160 | in_msg_body.end_parse();
161 |
162 | cell state_init = calculate_order_state_init(my_address(), order_seqno);
163 | slice order_address = calculate_address_by_state_init(BASECHAIN, state_init);
164 |
165 | throw_unless(error::unauthorized_execute, equal_slices_bits(sender_address, order_address));
166 | throw_unless(error::singers_outdated, (signers_hash == signers.cell_hash()) & (approvals_num >= threshold));
167 | throw_unless(error::expired, expiration_date >= now());
168 |
169 | (threshold, signers, signers_num, proposers)~execute_order(order_body);
170 | } elseif (op == op::execute_internal) {
171 | ;; we always trust ourselves, this feature is used to make chains of executions
172 | ;; where last action of previous execution triggers new one.
173 |
174 | throw_unless(error::unauthorized_execute, equal_slices_bits(sender_address, my_address()));
175 | cell order_body = in_msg_body~load_ref();
176 | in_msg_body.end_parse();
177 | (threshold, signers, signers_num, proposers)~execute_order(order_body);
178 | }
179 |
180 | save_data(next_order_seqno, threshold, signers, signers_num, proposers, allow_arbitrary_order_seqno);
181 | }
182 |
183 | (int, int, cell, cell) get_multisig_data() method_id {
184 | (int next_order_seqno, int threshold, cell signers, int signers_num, cell proposers, int allow_arbitrary_order_seqno) = load_data();
185 | throw_unless(error::inconsistent_data, signers_num == validate_dictionary_sequence(signers));
186 | validate_dictionary_sequence(proposers);
187 | throw_unless(error::invalid_signers, signers_num >= 1);
188 | throw_unless(error::invalid_threshold, threshold > 0);
189 | throw_unless(error::invalid_threshold, threshold <= signers_num);
190 | return (allow_arbitrary_order_seqno ? -1 : next_order_seqno, threshold, signers, proposers);
191 | }
192 |
193 | int get_order_estimate(cell order, int expiration_date) method_id {
194 | (_, _, cell signers, _, _, _) = load_data();
195 | return calculate_order_processing_cost(order, signers, expiration_date - now());
196 | }
197 |
198 | slice get_order_address(int order_seqno) method_id {
199 | cell state_init = calculate_order_state_init(my_address(), order_seqno);
200 | return calculate_address_by_state_init(BASECHAIN, state_init);
201 | }
202 |
--------------------------------------------------------------------------------
/contracts/multisig.tlb:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * FROM hashmap.tlb
4 | *
5 | */
6 | // ordinary Hashmap / HashmapE, with fixed length keys
7 | //
8 |
9 | bit$_ (## 1) = Bit;
10 | nothing$0 {X:Type} = Maybe X;
11 | just$1 {X:Type} value:X = Maybe X;
12 |
13 | hm_edge#_ {n:#} {X:Type} {l:#} {m:#} label:(HmLabel ~l n)
14 | {n = (~m) + l} node:(HashmapNode m X) = Hashmap n X;
15 |
16 | hmn_leaf#_ {X:Type} value:X = HashmapNode 0 X;
17 | hmn_fork#_ {n:#} {X:Type} left:^(Hashmap n X)
18 | right:^(Hashmap n X) = HashmapNode (n + 1) X;
19 |
20 | hml_short$0 {m:#} {n:#} len:(Unary ~n) {n <= m} s:(n * Bit) = HmLabel ~n m;
21 | hml_long$10 {m:#} n:(#<= m) s:(n * Bit) = HmLabel ~n m;
22 | hml_same$11 {m:#} v:Bit n:(#<= m) = HmLabel ~n m;
23 |
24 | unary_zero$0 = Unary ~0;
25 | unary_succ$1 {n:#} x:(Unary ~n) = Unary ~(n + 1);
26 |
27 | hme_empty$0 {n:#} {X:Type} = HashmapE n X;
28 | hme_root$1 {n:#} {X:Type} root:^(Hashmap n X) = HashmapE n X;
29 |
30 | // ============= Addresses
31 |
32 | addr_none$00 = MsgAddressExt;
33 | addr_extern$01 len:(## 9) external_address:(bits len)
34 | = MsgAddressExt;
35 | anycast_info$_ depth:(#<= 30) { depth >= 1 }
36 | rewrite_pfx:(bits depth) = Anycast;
37 | addr_std$10 anycast:(Maybe Anycast)
38 | workchain_id:int8 address:bits256 = MsgAddressInt;
39 | addr_var$11 anycast:(Maybe Anycast) addr_len:(## 9)
40 | workchain_id:int32 address:(bits addr_len) = MsgAddressInt;
41 |
42 |
43 |
44 | // ===================== Multisig =====================
45 |
46 |
47 | send_message#f1381e5b mode:uint8 message:^Cell = Action;
48 | update_multisig_param#1d0cfbd3 threshold:uint8
49 | signers:^(Hashmap 8 MsgAddressInt)
50 | proposers:(HashmapE 8 MsgAddressInt) = Action;
51 |
52 | _ _:(Hashmap 8 Action) = Order;
53 |
54 |
55 |
56 | new_order#f718510f query_id:uint64
57 | order_seqno:uint256
58 | signer:(## 1)
59 | index:uint8
60 | expiration_date:uint48
61 | order:^Order = InternalMsgBody;
62 | execute#75097f5d query_id:uint64
63 | order_seqno:uint256
64 | expiration_date:uint48
65 | approvals_num:uint8
66 | signers_hash:bits256
67 | order:^Order = InternalMsgBody;
68 | execute_internal#a32c59bf query_id:uint64 order:^Order = InternalMsgBody;
69 |
70 | // ===================== Order =====================
71 |
72 | //comment_approve#00000000617070726f7665 = InternalMsgBody;
73 |
74 | init#9c73fba2 query_id:uint64
75 | threshold:uint8
76 | signers:^(Hashmap 8 MsgAddressInt)
77 | expiration_date:uint48
78 | order:^Order
79 | approve_on_init:(## 1)
80 | signer_index:approve_on_init?uint8 = InternalMsgBody;
81 |
82 | approve#a762230f query_id:uint64 signer_index:uint8 = InternalMsgBody;
83 | approve_accepted#82609bf6 query_id:uint64 = InternalMsgBody;
84 | approve_rejected#afaf283e query_id:uint64 exit_code:uint32 = InternalMsgBody;
85 |
86 |
--------------------------------------------------------------------------------
/contracts/op-codes.func:
--------------------------------------------------------------------------------
1 | const int op::new_order = 0xf718510f;
2 | const int op::execute = 0x75097f5d;
3 | const int op::execute_internal = 0xa32c59bf;
4 |
5 | const int op::init = 0x9c73fba2;
6 | const int op::approve = 0xa762230f;
7 | const int op::approve_accepted = 0x82609bf6;
8 | const int op::approve_rejected = 0xafaf283e;
9 |
10 | const int actions::send_message = 0xf1381e5b;
11 | const int actions::update_multisig_params = 0x1d0cfbd3;
12 |
13 |
--------------------------------------------------------------------------------
/contracts/order.func:
--------------------------------------------------------------------------------
1 | #include "imports/stdlib.fc";
2 | #include "types.func";
3 | #include "op-codes.func";
4 | #include "messages.func";
5 | #include "errors.func";
6 |
7 | ;; DATA
8 |
9 | global slice multisig_address;
10 | global int order_seqno;
11 | global int threshold;
12 | global int sent_for_execution?;
13 | global cell signers;
14 | global int approvals_mask;
15 | global int approvals_num;
16 | global int expiration_date;
17 | global cell order;
18 |
19 | () load_data() impure inline {
20 | slice ds = get_data().begin_parse();
21 | multisig_address = ds~load_msg_addr();
22 | order_seqno = ds~load_order_seqno();
23 |
24 | if (ds.slice_bits() == 0) {
25 | ;; not initialized yet
26 | threshold = null();
27 | sent_for_execution? = null();
28 | signers = null();
29 | approvals_mask = null();
30 | approvals_num = null();
31 | expiration_date = null();
32 | order = null();
33 | } else {
34 | threshold = ds~load_index();
35 | sent_for_execution? = ds~load_bool();
36 | signers = ds~load_nonempty_dict();
37 | approvals_mask = ds~load_uint(MASK_SIZE);
38 | approvals_num = ds~load_index();
39 | expiration_date = ds~load_timestamp();
40 | order = ds~load_ref();
41 | ds.end_parse();
42 | }
43 | }
44 |
45 | () save_data() impure inline {
46 | set_data(
47 | begin_cell()
48 | .store_slice(multisig_address)
49 | .store_order_seqno(order_seqno)
50 | .store_index(threshold)
51 | .store_bool(sent_for_execution?)
52 | .store_nonempty_dict(signers)
53 | .store_uint(approvals_mask, MASK_SIZE)
54 | .store_index(approvals_num)
55 | .store_timestamp(expiration_date)
56 | .store_ref(order)
57 | .end_cell()
58 | );
59 | }
60 |
61 | ;; UTILS
62 |
63 | slice get_text_comment(slice in_msg_body) impure inline {
64 | if (in_msg_body.slice_refs() == 0) {
65 | return in_msg_body;
66 | }
67 |
68 | ;;combine comment into one slice
69 | builder combined_string = begin_cell();
70 | int need_exit = false;
71 | do {
72 | ;; store all bits from current cell
73 | ;; it's ok to overflow here, it means that comment is incorrect
74 | combined_string = combined_string.store_slice(in_msg_body.preload_bits(in_msg_body.slice_bits()));
75 | ;;and go to the next
76 |
77 | if (in_msg_body.slice_refs()) {
78 | in_msg_body = in_msg_body.preload_ref().begin_parse();
79 | } else {
80 | need_exit = true;
81 | }
82 |
83 | } until (need_exit);
84 | return combined_string.end_cell().begin_parse();
85 | }
86 |
87 | (int, int) find_signer_by_address(slice signer_address) impure inline {
88 | int found_signer? = false;
89 | int signer_index = -1;
90 | do {
91 | (signer_index, slice value, int next_found?) = signers.udict_get_next?(INDEX_SIZE, signer_index);
92 | if (next_found?) {
93 | if (equal_slices_bits(signer_address, value)) {
94 | found_signer? = true;
95 | next_found? = false; ;; fast way to exit loop
96 | }
97 | }
98 | } until (~ next_found?);
99 | return (signer_index, found_signer?);
100 | }
101 |
102 | () add_approval(int signer_index) impure inline {
103 | int mask = 1 << signer_index;
104 | throw_if(error::already_approved, approvals_mask & mask);
105 | approvals_num += 1;
106 | approvals_mask |= mask;
107 | }
108 |
109 | () try_execute(int query_id) impure inline_ref {
110 | if (approvals_num == threshold) {
111 | send_message_with_only_body(
112 | multisig_address,
113 | 0,
114 | begin_cell()
115 | .store_op_and_query_id(op::execute, query_id)
116 | .store_order_seqno(order_seqno)
117 | .store_timestamp(expiration_date)
118 | .store_index(approvals_num)
119 | .store_hash(signers.cell_hash())
120 | .store_ref(order),
121 | NON_BOUNCEABLE,
122 | SEND_MODE_CARRY_ALL_BALANCE | SEND_MODE_BOUNCE_ON_ACTION_FAIL
123 | );
124 | sent_for_execution? = true;
125 | }
126 | }
127 |
128 | () approve(int signer_index, slice response_address, int query_id) impure inline_ref {
129 | try {
130 | throw_if(error::already_executed, sent_for_execution?);
131 |
132 | add_approval(signer_index);
133 |
134 | send_message_with_only_body(
135 | response_address,
136 | 0,
137 | begin_cell().store_op_and_query_id(op::approve_accepted, query_id),
138 | NON_BOUNCEABLE,
139 | SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE | SEND_MODE_BOUNCE_ON_ACTION_FAIL
140 | );
141 |
142 | try_execute(query_id);
143 |
144 | save_data();
145 |
146 | } catch (_, exit_code) {
147 | send_message_with_only_body(
148 | response_address,
149 | 0,
150 | begin_cell()
151 | .store_op_and_query_id(op::approve_rejected, query_id)
152 | .store_uint(exit_code, 32),
153 | NON_BOUNCEABLE,
154 | SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE | SEND_MODE_BOUNCE_ON_ACTION_FAIL
155 | );
156 | }
157 | }
158 |
159 | ;; RECEIVE
160 |
161 | () recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) {
162 | slice in_msg_full_slice = in_msg_full.begin_parse();
163 | int msg_flags = in_msg_full_slice~load_msg_flags();
164 | if (msg_flags & 1) { ;; is bounced
165 | return ();
166 | }
167 | slice sender_address = in_msg_full_slice~load_msg_addr();
168 |
169 | int op = in_msg_body~load_op();
170 |
171 | load_data();
172 |
173 | if (op == 0) {
174 | ;; message with text comment
175 | slice text_comment = get_text_comment(in_msg_body);
176 | throw_unless(error::unknown_op, equal_slices_bits(text_comment, "approve"));
177 |
178 | (int signer_index, int found_signer?) = find_signer_by_address(sender_address);
179 | throw_unless(error::unauthorized_sign, found_signer?);
180 |
181 | approve(signer_index, sender_address, cur_lt());
182 | return ();
183 | }
184 |
185 | int query_id = in_msg_body~load_query_id();
186 |
187 | if (op == op::init) {
188 | throw_unless(error::unauthorized_init, equal_slices_bits(sender_address, multisig_address));
189 |
190 | if (null?(threshold)) {
191 | ;; Let's init
192 | threshold = in_msg_body~load_index();
193 | sent_for_execution? = false;
194 | signers = in_msg_body~load_nonempty_dict();
195 | approvals_mask = 0;
196 | approvals_num = 0;
197 | expiration_date = in_msg_body~load_timestamp();
198 | throw_unless(error::expired, expiration_date >= now()); ;; in case of error TONs will bounce back to multisig
199 | order = in_msg_body~load_ref();
200 |
201 | int approve_on_init? = in_msg_body~load_bool();
202 | if (approve_on_init?) {
203 | int signer_index = in_msg_body~load_index();
204 | add_approval(signer_index);
205 | try_execute(query_id);
206 | }
207 | in_msg_body.end_parse();
208 | save_data();
209 | return ();
210 | } else {
211 | ;; order is inited second time, if it is inited by another oracle
212 | ;; we count it as approval
213 | throw_unless(error::already_inited, in_msg_body~load_index() == threshold);
214 | throw_unless(error::already_inited, in_msg_body~load_nonempty_dict().cell_hash() == signers.cell_hash());
215 | throw_unless(error::already_inited,in_msg_body~load_timestamp() == expiration_date);
216 | throw_unless(error::already_inited, in_msg_body~load_ref().cell_hash() == order.cell_hash());
217 |
218 | int approve_on_init? = in_msg_body~load_bool();
219 | throw_unless(error::already_inited, approve_on_init?);
220 | int signer_index = in_msg_body~load_index();
221 | in_msg_body.end_parse();
222 | (slice signer_address, int found?) = signers.udict_get?(INDEX_SIZE, signer_index);
223 | throw_unless(error::unauthorized_sign, found?);
224 | approve(signer_index, signer_address, query_id);
225 | return ();
226 | }
227 | }
228 |
229 | if (op == op::approve) {
230 | int signer_index = in_msg_body~load_index();
231 | in_msg_body.end_parse();
232 | (slice signer_address, int found?) = signers.udict_get?(INDEX_SIZE, signer_index);
233 | throw_unless(error::unauthorized_sign, found?);
234 | throw_unless(error::unauthorized_sign, equal_slices_bits(sender_address, signer_address));
235 |
236 | approve(signer_index, sender_address, query_id);
237 | return ();
238 | }
239 |
240 | throw(error::unknown_op);
241 | }
242 |
243 | ;; GET-METHODS
244 |
245 | _ get_order_data() method_id {
246 | load_data();
247 | return (
248 | multisig_address,
249 | order_seqno,
250 | threshold,
251 | sent_for_execution?,
252 | signers,
253 | approvals_mask,
254 | approvals_num,
255 | expiration_date,
256 | order
257 | );
258 | }
--------------------------------------------------------------------------------
/contracts/order_helpers.func:
--------------------------------------------------------------------------------
1 | #include "imports/stdlib.fc";
2 | #include "types.func";
3 | #include "auto/order_code.func";
4 |
5 | cell pack_order_init_data(slice multisig_address, int order_seqno) inline {
6 | return begin_cell()
7 | .store_slice(multisig_address)
8 | .store_order_seqno(order_seqno)
9 | .end_cell();
10 | }
11 |
12 | cell calculate_order_state_init(slice multisig_address, int order_seqno) inline {
13 | {-
14 | https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L144
15 | _ split_depth:(Maybe (## 5)) special:(Maybe TickTock)
16 | code:(Maybe ^Cell) data:(Maybe ^Cell)
17 | library:(Maybe ^Cell) = StateInit;
18 | -}
19 | return begin_cell()
20 | .store_uint(0, 2) ;; 0b00 - No split_depth; No special
21 | .store_maybe_ref(order_code())
22 | .store_maybe_ref(pack_order_init_data(multisig_address, order_seqno))
23 | .store_uint(0, 1) ;; Empty libraries
24 | .end_cell();
25 | }
26 |
27 | slice calculate_address_by_state_init(int workchain, cell state_init) inline {
28 | {-
29 | https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L105
30 | addr_std$10 anycast:(Maybe Anycast) workchain_id:int8 address:bits256 = MsgAddressInt;
31 | -}
32 | return begin_cell()
33 | .store_uint(4, 3) ;; 0b100 = addr_std$10 tag; No anycast
34 | .store_int(workchain, 8)
35 | .store_uint(cell_hash(state_init), 256)
36 | .end_cell()
37 | .begin_parse();
38 | }
39 |
40 | ;;; @see /description.md "How is it calculated"
41 |
42 | const int MULTISIG_INIT_ORDER_GAS = 10232; ;; 255 signers 61634 gas total - size_counting gas(51402)
43 | const int ORDER_INIT_GAS = 4614;
44 | const int ORDER_EXECUTE_GAS = 11244; ;; 255 signers increases lookup costs
45 | const int MULTISIG_EXECUTE_GAS = 7576; ;; For single transfer action order
46 | ;; we call number of bits/cells without order bits/cells as "overhead"
47 | const int INIT_ORDER_BIT_OVERHEAD = 1337;
48 | const int INIT_ORDER_CELL_OVERHEAD = 6;
49 | const int ORDER_STATE_BIT_OVERHEAD = 1760;
50 | const int ORDER_STATE_CELL_OVERHEAD = 6;
51 | const int EXECUTE_ORDER_BIT_OVERHEAD = 664;
52 | const int EXECUTE_ORDER_CELL_OVERHEAD = 1;
53 |
54 | int calculate_order_processing_cost(cell order_body, cell signers, int duration) inline {
55 | {- There are following costs:
56 | 1) Gas cost on Multisig contract
57 | 2) Forward cost for Multisig->Order message
58 | 3) Gas cost on Order initialisation
59 | 4) Storage cost on Order
60 | 5) Gas cost on Order finalization
61 | 6) Forward cost for Order->Multisig message
62 | 7) Gas cost on Multisig till accept_message
63 | -}
64 |
65 | ;; compute_data_size is unpredictable in gas, so we need to measure gas prior to it and after
66 | ;; and add difference to MULTISIG_INIT_ORDER_GAS
67 | int initial_gas = gas_consumed();
68 | (int order_cells, int order_bits, _) = compute_data_size(order_body, 8192); ;; max cells in external message = 8192
69 | (int signers_cells, int signers_bits, _) = compute_data_size(signers, 512); ;; max 255 signers in dict, this max cells in dict = 511
70 | int size_counting_gas = gas_consumed() - initial_gas;
71 |
72 | int gas_fees = get_compute_fee(BASECHAIN,MULTISIG_INIT_ORDER_GAS + size_counting_gas) +
73 | get_compute_fee(BASECHAIN, ORDER_INIT_GAS) +
74 | get_compute_fee(BASECHAIN, ORDER_EXECUTE_GAS) +
75 | get_compute_fee(BASECHAIN, MULTISIG_EXECUTE_GAS);
76 |
77 | int forward_fees = get_forward_fee(BASECHAIN,
78 | INIT_ORDER_BIT_OVERHEAD + order_bits + signers_bits,
79 | INIT_ORDER_CELL_OVERHEAD + order_cells + signers_cells) +
80 | get_forward_fee(BASECHAIN,
81 | EXECUTE_ORDER_BIT_OVERHEAD + order_bits,
82 | EXECUTE_ORDER_CELL_OVERHEAD + order_cells);
83 |
84 |
85 | int storage_fees = get_storage_fee(BASECHAIN, duration,
86 | ORDER_STATE_BIT_OVERHEAD + order_bits + signers_bits,
87 | ORDER_STATE_CELL_OVERHEAD + order_cells + signers_cells);
88 | return gas_fees + forward_fees + storage_fees;
89 | }
90 |
--------------------------------------------------------------------------------
/contracts/types.func:
--------------------------------------------------------------------------------
1 | ;; Multisig types
2 |
3 | #include "imports/stdlib.fc";
4 |
5 | ;; Alias for load_ref
6 | (slice, cell) load_nonempty_dict(slice s) asm(-> 1 0) "LDREF";
7 |
8 | ;; alias for store_ref
9 | builder store_nonempty_dict(builder b, cell c) asm(c b) "STREF";
10 |
11 | const int TIMESTAMP_SIZE = 48;
12 |
13 | (slice, int) ~load_timestamp(slice s) inline {
14 | return s.load_uint(TIMESTAMP_SIZE);
15 | }
16 | builder store_timestamp(builder b, int timestamp) inline {
17 | return b.store_uint(timestamp, TIMESTAMP_SIZE);
18 | }
19 |
20 | const int HASH_SIZE = 256;
21 |
22 | (slice, int) ~load_hash(slice s) inline {
23 | return s.load_uint(HASH_SIZE);
24 | }
25 | builder store_hash(builder b, int hash) inline {
26 | return b.store_uint(hash, HASH_SIZE);
27 | }
28 |
29 | {- By index we mean index of signer in signers dictionary. The same type is used
30 | for threshold, singers number and for proposers indexes -}
31 | const int INDEX_SIZE = 8;
32 | const int MASK_SIZE = 1 << INDEX_SIZE;
33 |
34 | (slice, int) ~load_index(slice s) inline {
35 | return s.load_uint(INDEX_SIZE);
36 | }
37 | builder store_index(builder b, int index) inline {
38 | return b.store_uint(index, INDEX_SIZE);
39 | }
40 |
41 | const int ACTION_INDEX_SIZE = 8;
42 |
43 |
44 | const int ORDER_SEQNO_SIZE = 256;
45 | const int MAX_ORDER_SEQNO = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
46 |
47 | (slice, int) ~load_order_seqno(slice s) inline {
48 | return s.load_uint(ORDER_SEQNO_SIZE);
49 | }
50 | builder store_order_seqno(builder b, int seqno) inline {
51 | return b.store_uint(seqno, ORDER_SEQNO_SIZE);
52 | }
--------------------------------------------------------------------------------
/description.md:
--------------------------------------------------------------------------------
1 | # Description
2 |
3 | ## What is order?
4 |
5 | Order in essence is sequential list of one or more actions executed by multisig wallet contract.
6 |
7 | ## Order contract state
8 |
9 | - `multisig_address` parent multisig address.
10 | - `order_seqno` sequential number of the order contract.
11 | - `threshold` number of signatures required to start order execution.
12 | - `sent_for_execution?` flag indication whether order has been executed already
13 | - `signers` Dictionary containing contract addresses allowed to sign order execution
14 | - `approvals_mask` Bit field where `true` bit at n-th position indicates approval granted from n-th signer.
15 | - `approvals_num` Total number of granted approvals.
16 | - `expiration_date` Once current time exceeds this timestamp, order can't be executed anymore.
17 | - `order` Cell containing action descriptors.
18 |
19 |
20 | ### Order actions
21 |
22 | #### Message
23 |
24 | When executing message order, multisig will send arbitrary message described by action.
25 |
26 | #### Multisig parameters update
27 |
28 | When executing update order, multisig will update it's parameters according to action.
29 |
30 | Updatable parameters are:
31 | - `signers`
32 | - `threshold`
33 |
34 | ## Order life cycle
35 |
36 | Order life cycle consists of following steps:
37 |
38 | - Order initialization.
39 | - Order approval(sign).
40 | - Order execution.
41 |
42 | ## Execution guarantees
43 |
44 | - Order can only be executed once. If execution is unsuccessful (no enough TON on Mutlisig, signer list changes, threshold became higher) Order can not be reused; new Order should be created and approved.
45 | - Order actions are executed one-by-one sequentially, that means Multisignature contract sends messages in the same order they are specified in Order (note that if destination of messages are in different shards, messages will be delivered asynchronously, possibly in different order).
46 | - Order can't be executed after it's expiration date.
47 | - Once approval is granted by signer, it can't be revoked.
48 |
49 | Note that in guarantees above Order means the totality of all parameters, in particular order id, list of order actions, creation and expiration dates, list of approvals, etc.
50 | It is possible to create two different Orders which share some parameters (for instance list of actions), however such orders will be independent and require separate approvals to be executed.
51 | Also, after Order execution, it's contract on-chain is no longer used. Given rental mechanism of TON blockchain that means that after some time (usually years) this contract will be removed from blockchain. After that, if Multisig operates in `allow_arbitrary_order_seqno` mode, a new Order with the same order_id can be created. Same as in the case above, two Orders that share order_id will be completely independent, can contain different list of actions and will require separate approvals (besides being separated in time by years)
52 |
53 | ### The order actions on the multisig are performed by the balance of the multisig
54 |
55 | Make sure you have enough balance on your multisig.
56 |
57 | ## Order initialization
58 |
59 | [Initialization message](https://github.com/ton-blockchain/multisig-contract-v2/blob/master/contracts/multisig.tlb#L74) is sent from `multisig` wallet, creating new order contract.
60 | Only `multisig_address` and `order_seqno` are part of [InitState](https://docs.ton.org/develop/data-formats/msg-tlb#stateinit-tl-b) structure (defining future contract address).
61 | Rest of the order state parameters are passed in a message body.
62 | `approve_on_init` parameter set to `true` indicates that initializer wants to sign for order during the initialization.
63 |
64 | ### Transaction chain
65 | `wallet->multisig->new order`
66 |
67 | ## Order approval
68 |
69 | Order approval may be granted either by [initialization](https://github.com/ton-blockchain/multisig-contract-v2/blob/master/contracts/multisig.tlb#L74) or [approve](https://github.com/ton-blockchain/multisig-contract-v2/blob/master/contracts/multisig.tlb#L82) message.
70 |
71 | `signer_index` field indicates index in `signers` dictionary to check sender address against.
72 |
73 | Approval by init will only be accepted from multisig address, where approve
74 | message will be accepted if sender address match the address
75 | at `signers[signer_index]`.
76 |
77 | ### Transaction chain
78 |
79 | `multisig->order`
80 |
81 | ## Order execution
82 |
83 | Once number of approvals reaches `threshold`, the
84 | [execute message](https://github.com/ton-blockchain/multisig-contract-v2/blob/master/contracts/multisig.tlb#L62)
85 | is sent back to multisig contract among with the whole order contract balance.
86 | Multisig contract [performs checks](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/contracts/multisig.func#L142)
87 | on the input order data and accepts it
88 | for execution if all checks passed successfully
89 |
90 | ### Transaction chain
91 |
92 | `order->multisig`
93 |
94 | ## Required order contract balance
95 |
96 | Order contract balance should be enough for:
97 |
98 | - Storage till `expiration_date`.
99 | - Execution of the multisig contract till order is [accepted](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/contracts/multisig.func#L23) for execution
100 |
101 | ## Order fees calculation
102 |
103 | During order initialization we must make sure that message value
104 | would be enough to cover:
105 |
106 | - Order initialization transaction chain.
107 | - Order storage
108 | - Execution of approve logic in case `approve_on_init` is `true`.
109 | - Order execution transaction chain in case `approve_on_init` is `true` and `threshold = 1`
110 |
111 | ### How is it calculated
112 |
113 | Fees are calculated in [order_helper.fc#53](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/contracts/order_helpers.func#L53)
114 |
115 | Constants are used as a base values, however
116 | size of signers cell and order body size is dynamic.
117 | Therefore dynamically calculated sizes and gas consumption is added to those base values.
118 |
119 | ``` func
120 | int initial_gas = gas_consumed();
121 | (int order_cells, int order_bits, _) = compute_data_size(order_body, 8192);
122 | (int signers_cells, int signers_bits, _) = compute_data_size(signers, 512);
123 | int size_counting_gas = gas_consumed() - initial_gas;
124 | ```
125 |
126 | To get these gas consumption constants, we use two test cases.
127 | - With small signers size:[tests/FeeComputation.spec.ts#L199](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/tests/FeeComputation.spec.ts#L199) referred as "small signers"
128 | - With maximum signers size:[tests/FeeComputation.spec.ts#L205](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/tests/FeeComputation.spec.ts#L205) referred as "large test"
129 |
130 |
131 | [contracts/order_helpers.func#L63](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/contracts/order_helpers.func#L63)
132 |
133 | #### Forward fees
134 |
135 | Forward fees are required to cover message sending.
136 | For all of the overheads, the `small signers` test is used.
137 |
138 | Related constants:
139 |
140 | `INIT_ORDER_BITS_OVERHEAD` and `INIT_ORDER_CELL_OVERHEAD`
141 | Represent total bits and cells used by order init message reduced by
142 | bits and cells occupied by order body.
143 | [tests/FeeComputation.spec.ts#L123](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/tests/FeeComputation.spec.ts#L123)
144 | ``` javascript
145 | /*
146 | tx0 : external -> treasury
147 | tx1: treasury -> multisig
148 | tx2: multisig -> order
149 | */
150 | let orderBody = (await order.getOrderData()).order;
151 | let orderBodyStats = collectCellStats(orderBody!, []);
152 |
153 | let multisigToOrderMessage = res.transactions[2].inMessage!;
154 | let multisigToOrderMessageStats = computeMessageForwardFees(curMsgPrices, multisigToOrderMessage).stats;
155 | let initOrderStateOverhead = multisigToOrderMessageStats.sub(orderBodyStats);
156 |
157 | ```
158 |
159 | `EXECUTE_ORDER_BIT_OVERHEAD` and `EXECUTE_ORDER_CELL_OVERHEAD`
160 | follow the same logic, but for execute message `order->multisig`.
161 | [tests/FeeComputation.spec.ts#L149](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/tests/FeeComputation.spec.ts#L149)
162 |
163 | ``` javascript
164 | /*
165 | tx0 : external -> treasury
166 | tx1: treasury -> order
167 | tx2: order -> treasury (approve)
168 | tx3: order -> multisig
169 | tx4+: multisig -> destination
170 | */
171 | let orderToMultiownerMessage = secondApproval.transactions[3].inMessage!;
172 | let orderToMultiownerMessageStats = computeMessageForwardFees(curMsgPrices, orderToMultiownerMessage).stats;
173 | let orderToMultiownerMessageOverhead = orderToMultiownerMessageStats.sub(orderBodyStats);
174 | ```
175 |
176 | Adds up with dynamic order body and signer sizes for
177 | calculation of the related forward fees.
178 |
179 | ``` func
180 | int forward_fees = get_forward_fee(BASECHAIN,
181 | INIT_ORDER_BIT_OVERHEAD + order_bits + signers_bits,
182 | INIT_ORDER_CELL_OVERHEAD + order_cells + signers_cells) +
183 | get_forward_fee(BASECHAIN,
184 | EXECUTE_ORDER_BIT_OVERHEAD + order_bits,
185 | EXECUTE_ORDER_CELL_OVERHEAD + order_cells);
186 |
187 | ```
188 |
189 | #### Gas fees
190 |
191 | Gas units are bound to the TVM instructions being executed
192 | where gas fees may change with the network configuration.
193 | Thus we rely on gas units.
194 |
195 | [contracts/order_helpers.func#L75](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/contracts/order_helpers.func#L75)
196 |
197 | `MULTISIG_INIT_ORDER_GAS` represents total gas units
198 | required for execution of `op::new_order` on multisig side.
199 | `large signers` test is used, and then `size_counting_gas` is deducted.
200 | In order to get `size_counting_gas`, one would have to dump the value manually.
201 |
202 | ``` func
203 | int size_counting_gas = gas_consumed() - initial_gas;
204 | size_counting_gas~dump();
205 |
206 | ```
207 | record the value and restore the source file.
208 |
209 | [tests/FeeComputation.spec.ts#L106](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/tests/FeeComputation.spec.ts#L106)
210 | ``` javascript
211 | /*
212 | tx0 : external -> treasury
213 | tx1: treasury -> multisig
214 | tx2: multisig -> order
215 | */
216 |
217 | let MULTISIG_INIT_ORDER_GAS = computedGeneric(res.transactions[1]).gasUsed;
218 | ```
219 |
220 | `ORDER_INIT_GAS` is amount of gas
221 | consumed by order contract while processing `op::init`.
222 | `small signers` test is used due to it is not impacted by signers or order size.
223 |
224 | [tests/FeeComputation.spec.ts#L108](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/tests/FeeComputation.spec.ts#L108)
225 |
226 | ``` javascript
227 | let ORDER_INIT_GAS = computedGeneric(res.transactions[2]).gasUsed;
228 | ```
229 |
230 | `ORDER_EXECUTE_GAS` is amount of gas consumed by order contract
231 | once execution threshold is reached.
232 | `large signers` test is used, because dictionary lookup cost depends on dictionary size.
233 | [contracts/order.func#L109](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/contracts/order.func#L109)
234 | Calculations:
235 | [tests/FeeComputation.spec.ts#L157](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/tests/FeeComputation.spec.ts#L157)
236 |
237 | ``` javascript
238 | let ORDER_EXECUTE_GAS = computedGeneric(secondApproval.transactions[1]).gasUsed;
239 | ```
240 |
241 | `MULTISIG_EXECUTE_GAS` is amount of gas consumed
242 | by multisig prior to accepting order execution.
243 | For simplicity we use cost of execution of a 1 message order.
244 | `small signers` test is used, due to it is not impacted by signers size.
245 | [tests/FeeComputation.spec.ts#L159](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/tests/FeeComputation.spec.ts#L159)
246 | ``` javascript
247 | let MULTISIG_EXECUTE_GAS = actions.length > 1 ? 7310n : computedGeneric(secondApproval.transactions[3]).gasUsed;
248 | ```
249 | While for a fact it's only required to cover gas till [contracts/multisig.func#L23](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/contracts/multisig.func#L23)
250 |
251 | These constants summ up with `size_counting` dynamic value and gas fee is calculated for each
252 | one of those separately.
253 |
254 | [contracts/order_helpers.func#L70](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/contracts/order_helpers.func#L70)
255 | ``` func
256 | int gas_fees = get_compute_fee(BASECHAIN,MULTISIG_INIT_ORDER_GAS + size_counting_gas) +
257 | get_compute_fee(BASECHAIN, ORDER_INIT_GAS) +
258 | get_compute_fee(BASECHAIN, ORDER_EXECUTE_GAS) +
259 | get_compute_fee(BASECHAIN, MULTISIG_EXECUTE_GAS);
260 | ```
261 |
262 | #### Storage fees
263 |
264 | `ORDER_STATE_BIT_OVERHEAD` and `ORDER_STATE_CELL_OVERHEAD` is how many bits and
265 | cells is occupied by order contract state without order body.
266 | `small signers` test is used, because `signers` and `order` overhead is added
267 | dynamically
268 | [tests/FeeComputation.spec.ts#L125](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/tests/FeeComputation.spec.ts#L125)
269 | ``` javascript
270 | let initOrderStateOverhead = multisigToOrderMessageStats.sub(orderBodyStats);
271 | ```
272 |
273 | [contracts/order_helpers.func#L83](https://github.com/ton-blockchain/multisig-contract-v2/blob/0c7eb74064fea6a77c7a29c0a11d357588b2fceb/contracts/order_helpers.func#L83)
274 | ``` func
275 | int storage_fees = get_storage_fee(BASECHAIN, duration,
276 | ORDER_STATE_BIT_OVERHEAD + order_bits + signers_bits,
277 | ORDER_STATE_CELL_OVERHEAD + order_cells + signers_cells);
278 |
279 | ```
280 |
--------------------------------------------------------------------------------
/gasUtils.ts:
--------------------------------------------------------------------------------
1 | import { Cell, Slice, toNano, beginCell, Address, Dictionary, Message, DictionaryValue, Transaction } from '@ton/core';
2 |
3 | export type GasPrices = {
4 | flat_gas_limit: bigint,
5 | flat_gas_price: bigint,
6 | gas_price: bigint;
7 | };
8 | export type StorageValue = {
9 | utime_sice: number,
10 | bit_price_ps: bigint,
11 | cell_price_ps: bigint,
12 | mc_bit_price_ps: bigint,
13 | mc_cell_price_ps: bigint
14 | };
15 |
16 |
17 | export type MsgPrices = ReturnType;
18 | export type FullFees = ReturnType;
19 |
20 | export class StorageStats {
21 | bits: bigint;
22 | cells: bigint;
23 |
24 | constructor(bits?: number | bigint, cells?: number | bigint) {
25 | this.bits = bits !== undefined ? BigInt(bits) : 0n;
26 | this.cells = cells !== undefined ? BigInt(cells) : 0n;
27 | }
28 | add(...stats: StorageStats[]) {
29 | let cells = this.cells, bits = this.bits;
30 | for (let stat of stats) {
31 | bits += stat.bits;
32 | cells += stat.cells;
33 | }
34 | return new StorageStats(bits, cells);
35 | }
36 | sub(...stats: StorageStats[]) {
37 | let cells = this.cells, bits = this.bits;
38 | for (let stat of stats) {
39 | bits -= stat.bits;
40 | cells -= stat.cells;
41 | }
42 | return new StorageStats(bits, cells);
43 | }
44 | addBits(bits: number | bigint) {
45 | return new StorageStats(this.bits + BigInt(bits), this.cells);
46 | }
47 | subBits(bits: number | bigint) {
48 | return new StorageStats(this.bits - BigInt(bits), this.cells);
49 | }
50 | addCells(cells: number | bigint) {
51 | return new StorageStats(this.bits, this.cells + BigInt(cells));
52 | }
53 | subCells(cells: number | bigint) {
54 | return new StorageStats(this.bits, this.cells - BigInt(cells));
55 | }
56 |
57 | toString() : string {
58 | return JSON.stringify({
59 | bits: this.bits.toString(),
60 | cells: this.cells.toString()
61 | });
62 | }
63 | }
64 |
65 | export function computedGeneric(transaction: T) {
66 | if(transaction.description.type !== "generic")
67 | throw("Expected generic transactionaction");
68 | if(transaction.description.computePhase.type !== "vm")
69 | throw("Compute phase expected")
70 | return transaction.description.computePhase;
71 | }
72 |
73 | export function storageGeneric(transaction: T) {
74 | if(transaction.description.type !== "generic")
75 | throw("Expected generic transactionaction");
76 | const storagePhase = transaction.description.storagePhase;
77 | if(storagePhase === null || storagePhase === undefined)
78 | throw("Storage phase expected")
79 | return storagePhase;
80 | }
81 |
82 | export function shr16ceil(src: bigint) {
83 | let rem = src % BigInt(65536);
84 | let res = src / 65536n; // >> BigInt(16);
85 | if (rem != BigInt(0)) {
86 | res += BigInt(1);
87 | }
88 | return res;
89 | }
90 |
91 | export function collectCellStats(cell: Cell, visited:Array, skipRoot: boolean = false): StorageStats {
92 | let bits = skipRoot ? 0n : BigInt(cell.bits.length);
93 | let cells = skipRoot ? 0n : 1n;
94 | let hash = cell.hash().toString();
95 | if (visited.includes(hash)) {
96 | // We should not account for current cell data if visited
97 | return new StorageStats();
98 | }
99 | else {
100 | visited.push(hash);
101 | }
102 | for (let ref of cell.refs) {
103 | let r = collectCellStats(ref, visited);
104 | cells += r.cells;
105 | bits += r.bits;
106 | }
107 | return new StorageStats(bits, cells);
108 | }
109 |
110 | export function getGasPrices(configRaw: Cell, workchain: 0 | -1): GasPrices {
111 | const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell());
112 |
113 | const ds = config.get(21 + workchain)!.beginParse();
114 | if(ds.loadUint(8) !== 0xd1) {
115 | throw new Error("Invalid flat gas prices tag!");
116 | }
117 |
118 | const flat_gas_limit = ds.loadUintBig(64);
119 | const flat_gas_price = ds.loadUintBig(64);
120 |
121 | if(ds.loadUint(8) !== 0xde) {
122 | throw new Error("Invalid gas prices tag!");
123 | }
124 | return {
125 | flat_gas_limit,
126 | flat_gas_price,
127 | gas_price: ds.preloadUintBig(64)
128 | };
129 | }
130 |
131 | export function setGasPrice(configRaw: Cell, prices: GasPrices, workchain: 0 | -1) : Cell {
132 | const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell());
133 | const idx = 21 + workchain;
134 | const ds = config.get(idx)!;
135 | const tail = ds.beginParse().skip(8 + 64 + 64 + 8 + 64);
136 |
137 | const newPrices = beginCell().storeUint(0xd1, 8)
138 | .storeUint(prices.flat_gas_limit, 64)
139 | .storeUint(prices.flat_gas_price, 64)
140 | .storeUint(0xde, 8)
141 | .storeUint(prices.gas_price, 64)
142 | .storeSlice(tail)
143 | .endCell();
144 | config.set(idx, newPrices);
145 |
146 | return beginCell().storeDictDirect(config).endCell();
147 | }
148 |
149 | export const storageValue : DictionaryValue = {
150 | serialize: (src, builder) => {
151 | builder.storeUint(0xcc, 8)
152 | .storeUint(src.utime_sice, 32)
153 | .storeUint(src.bit_price_ps, 64)
154 | .storeUint(src.cell_price_ps, 64)
155 | .storeUint(src.mc_bit_price_ps, 64)
156 | .storeUint(src.mc_cell_price_ps, 64)
157 | },
158 | parse: (src) => {
159 | return {
160 | utime_sice: src.skip(8).loadUint(32),
161 | bit_price_ps: src.loadUintBig(64),
162 | cell_price_ps: src.loadUintBig(64),
163 | mc_bit_price_ps: src.loadUintBig(64),
164 | mc_cell_price_ps: src.loadUintBig(64)
165 | };
166 | }
167 | };
168 |
169 | export function getStoragePrices(configRaw: Cell) {
170 | const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell());
171 | const storageData = Dictionary.loadDirect(Dictionary.Keys.Uint(32),storageValue, config.get(18)!);
172 | const values = storageData.values();
173 |
174 | return values[values.length - 1];
175 | }
176 | export function calcStorageFee(prices: StorageValue, stats: StorageStats, duration: bigint) {
177 | return shr16ceil((stats.bits * prices.bit_price_ps + stats.cells * prices.cell_price_ps) * duration)
178 | }
179 | export function setStoragePrices(configRaw: Cell, prices: StorageValue) {
180 | const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell());
181 | const storageData = Dictionary.loadDirect(Dictionary.Keys.Uint(32),storageValue, config.get(18)!);
182 | storageData.set(storageData.values().length - 1, prices);
183 | config.set(18, beginCell().storeDictDirect(storageData).endCell());
184 | return beginCell().storeDictDirect(config).endCell();
185 | }
186 |
187 | export function computeGasFee(prices: GasPrices, gas: bigint): bigint {
188 | if(gas <= prices.flat_gas_limit) {
189 | return prices.flat_gas_price;
190 | }
191 | return prices.flat_gas_price + prices.gas_price * (gas - prices.flat_gas_limit) / 65536n
192 | }
193 |
194 | export function computeDefaultForwardFee(msgPrices: MsgPrices) {
195 | return msgPrices.lumpPrice - ((msgPrices.lumpPrice * msgPrices.firstFrac) >> BigInt(16));
196 | }
197 |
198 | export function computeCellForwardFees(msgPrices: MsgPrices, msg: Cell) {
199 | let storageStats = collectCellStats(msg, [], true);
200 | return computeFwdFees(msgPrices, storageStats.cells, storageStats.bits);
201 | }
202 | export function computeMessageForwardFees(msgPrices: MsgPrices, msg: Message) {
203 | // let msg = loadMessageRelaxed(cell.beginParse());
204 | let storageStats = new StorageStats();
205 |
206 | if( msg.info.type !== "internal") {
207 | throw Error("Helper intended for internal messages");
208 | }
209 | const defaultFwd = computeDefaultForwardFee(msgPrices);
210 | // If message forward fee matches default than msg cell is flat
211 | if(msg.info.forwardFee == defaultFwd) {
212 | return {fees: msgPrices.lumpPrice, res : defaultFwd, remaining: defaultFwd, stats: storageStats};
213 | }
214 | let visited : Array = [];
215 | // Init
216 | if (msg.init) {
217 | let addBits = 5n; // Minimal additional bits
218 | let refCount = 0;
219 | if(msg.init.splitDepth) {
220 | addBits += 5n;
221 | }
222 | if(msg.init.libraries) {
223 | refCount++;
224 | storageStats = storageStats.add(collectCellStats(beginCell().storeDictDirect(msg.init.libraries).endCell(), visited, true));
225 | }
226 | if(msg.init.code) {
227 | refCount++;
228 | storageStats = storageStats.add(collectCellStats(msg.init.code, visited))
229 | }
230 | if(msg.init.data) {
231 | refCount++;
232 | storageStats = storageStats.add(collectCellStats(msg.init.data, visited));
233 | }
234 | if(refCount >= 2) { //https://github.com/ton-blockchain/ton/blob/51baec48a02e5ba0106b0565410d2c2fd4665157/crypto/block/transaction.cpp#L2079
235 | storageStats.cells++;
236 | storageStats.bits += addBits;
237 | }
238 | }
239 | const lumpBits = BigInt(msg.body.bits.length);
240 | const bodyStats = collectCellStats(msg.body,visited, true);
241 | storageStats = storageStats.add(bodyStats);
242 |
243 | // NOTE: Extra currencies are ignored for now
244 | let fees = computeFwdFeesVerbose(msgPrices, BigInt(storageStats.cells), BigInt(storageStats.bits));
245 | // Meeh
246 | if(fees.remaining < msg.info.forwardFee) {
247 | // console.log(`Remaining ${fees.remaining} < ${msg.info.forwardFee} lump bits:${lumpBits}`);
248 | storageStats = storageStats.addCells(1).addBits(lumpBits);
249 | fees = computeFwdFeesVerbose(msgPrices, storageStats.cells, storageStats.bits);
250 | }
251 | if(fees.remaining != msg.info.forwardFee) {
252 | console.log("Result fees:", fees);
253 | console.log(msg);
254 | console.log(fees.remaining);
255 | throw(new Error("Something went wrong in fee calcuation!"));
256 | }
257 | return {fees, stats: storageStats};
258 | }
259 |
260 | export const configParseMsgPrices = (sc: Slice) => {
261 |
262 | let magic = sc.loadUint(8);
263 |
264 | if(magic != 0xea) {
265 | throw Error("Invalid message prices magic number!");
266 | }
267 | return {
268 | lumpPrice:sc.loadUintBig(64),
269 | bitPrice: sc.loadUintBig(64),
270 | cellPrice: sc.loadUintBig(64),
271 | ihrPriceFactor: sc.loadUintBig(32),
272 | firstFrac: sc.loadUintBig(16),
273 | nextFrac: sc.loadUintBig(16)
274 | };
275 | }
276 |
277 | export const setMsgPrices = (configRaw: Cell, prices: MsgPrices, workchain: 0 | -1) => {
278 | const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell());
279 |
280 | const priceCell = beginCell().storeUint(0xea, 8)
281 | .storeUint(prices.lumpPrice, 64)
282 | .storeUint(prices.bitPrice, 64)
283 | .storeUint(prices.cellPrice, 64)
284 | .storeUint(prices.ihrPriceFactor, 32)
285 | .storeUint(prices.firstFrac, 16)
286 | .storeUint(prices.nextFrac, 16)
287 | .endCell();
288 | config.set(25 + workchain, priceCell);
289 |
290 | return beginCell().storeDictDirect(config).endCell();
291 | }
292 |
293 | export const getMsgPrices = (configRaw: Cell, workchain: 0 | -1 ) => {
294 |
295 | const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell());
296 |
297 | const prices = config.get(25 + workchain);
298 |
299 | if(prices === undefined) {
300 | throw Error("No prices defined in config");
301 | }
302 |
303 | return configParseMsgPrices(prices.beginParse());
304 | }
305 |
306 | export function computeFwdFees(msgPrices: MsgPrices, cells: bigint, bits: bigint) {
307 | return msgPrices.lumpPrice + (shr16ceil((msgPrices.bitPrice * bits)
308 | + (msgPrices.cellPrice * cells))
309 | );
310 | }
311 |
312 | export function computeFwdFeesVerbose(msgPrices: MsgPrices, cells: bigint | number, bits: bigint | number) {
313 | const fees = computeFwdFees(msgPrices, BigInt(cells), BigInt(bits));
314 |
315 | const res = (fees * msgPrices.firstFrac) >> 16n;
316 | return {
317 | total: fees,
318 | res,
319 | remaining: fees - res
320 | }
321 | }
322 |
--------------------------------------------------------------------------------
/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 | maxWorkers: 1
8 | };
9 |
10 | export default config;
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Multiowner",
3 | "version": "0.0.1",
4 | "scripts": {
5 | "start": "blueprint run",
6 | "build": "blueprint build",
7 | "test": "jest"
8 | },
9 | "devDependencies": {
10 | "@ton/blueprint": "^0.15.0",
11 | "@ton/core": "^0.54.0",
12 | "@ton/crypto": "^3.2.0",
13 | "@ton/sandbox": "0.16.0-tvmbeta.3",
14 | "@ton/test-utils": "^0.4.2",
15 | "@ton/ton": "^13.10.0",
16 | "@types/jest": "^29.5.0",
17 | "@types/node": "^20.2.5",
18 | "jest": "^29.5.0",
19 | "prettier": "^2.8.6",
20 | "ts-jest": "^29.0.5",
21 | "ts-node": "^10.9.1",
22 | "typescript": "^4.9.5"
23 | },
24 | "overrides": {
25 | "@ton-community/func-js-bin": "0.4.5-tvmbeta.3",
26 | "@ton-community/func-js": "0.6.3-tvmbeta.3",
27 | "@ton-community/sandbox": "0.16.0-tvmbeta.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/scripts/deployLibrary.ts:
--------------------------------------------------------------------------------
1 | import {toNano} from '@ton/core';
2 | import {compile, NetworkProvider} from '@ton/blueprint';
3 | import {Librarian} from "../wrappers/Librarian";
4 |
5 | export async function run(provider: NetworkProvider) {
6 | const order_code_raw = await compile('Order');
7 |
8 | // deploy lib
9 |
10 | const librarian_code = await compile('Librarian');
11 | const librarian = provider.open(Librarian.createFromConfig({code: order_code_raw}, librarian_code));
12 | await librarian.sendDeploy(provider.sender(), toNano("10"));
13 | }
14 |
--------------------------------------------------------------------------------
/scripts/deployMultiownerWallet.ts:
--------------------------------------------------------------------------------
1 | import {Address, toNano} from '@ton/core';
2 | import {Multisig} from '../wrappers/Multisig';
3 | import {compile, NetworkProvider} from '@ton/blueprint';
4 |
5 | export async function run(provider: NetworkProvider) {
6 | const multisig_code = await compile('Multisig');
7 |
8 | // deploy multisig
9 |
10 | const multiownerWallet = provider.open(Multisig.createFromConfig({
11 | threshold: 2,
12 | signers: [Address.parse('UQBONmT67oFPvbbByzbXK6xS0V4YbBHs1mT-Gz8afP2AHdyt'), Address.parse('0QAR0lJjOVUzyT4QBKg50k216RBqvpvEPlq2_xGtdMkgFgcY'), Address.parse('UQAGkOdcs7i0OomLkySkVdiLbzriH4ptQAgYWqHRVK2vXO4z')],
13 | proposers: [Address.parse('0QAR0lJjOVUzyT4QBKg50k216RBqvpvEPlq2_xGtdMkgFgcY')],
14 | allowArbitrarySeqno: true
15 | }, multisig_code));
16 |
17 | await multiownerWallet.sendDeploy(provider.sender(), toNano('0.05'));
18 | await provider.waitForDeploy(multiownerWallet.address);
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/scripts/newOrder.ts:
--------------------------------------------------------------------------------
1 | import {Address, beginCell, toNano} from '@ton/core';
2 | import {Multisig} from '../wrappers/Multisig';
3 | import {compile, NetworkProvider} from '@ton/blueprint';
4 |
5 | export async function run(provider: NetworkProvider) {
6 | const multisig_code = await compile('Multisig');
7 |
8 | // deploy multisig
9 |
10 | const multiownerWallet = provider.open(Multisig.createFromConfig({
11 | threshold: 2,
12 | signers: [Address.parse('UQBONmT67oFPvbbByzbXK6xS0V4YbBHs1mT-Gz8afP2AHdyt'), Address.parse('0QAR0lJjOVUzyT4QBKg50k216RBqvpvEPlq2_xGtdMkgFgcY'), Address.parse('UQAGkOdcs7i0OomLkySkVdiLbzriH4ptQAgYWqHRVK2vXO4z')],
13 | proposers: [Address.parse('0QAR0lJjOVUzyT4QBKg50k216RBqvpvEPlq2_xGtdMkgFgcY')],
14 | allowArbitrarySeqno: true
15 | }, multisig_code));
16 |
17 | // create new order
18 |
19 | const masterMsg = beginCell()
20 | .storeUint(0x178d4519, 32) // internal_transfer
21 | .storeUint(0, 64) // query_id
22 | .storeCoins(5000000000n) // jetton amount
23 | .storeAddress(Address.parse('0QAR0lJjOVUzyT4QBKg50k216RBqvpvEPlq2_xGtdMkgFgcY')) // from address (will be ignored)
24 | .storeAddress(Address.parse('0QAR0lJjOVUzyT4QBKg50k216RBqvpvEPlq2_xGtdMkgFgcY')) // response address
25 | .storeCoins(0) // forward payload
26 | .storeBit(false) // no forward
27 | .endCell();
28 |
29 | await multiownerWallet.sendNewOrder(provider.sender(), [{
30 | type: 'transfer',
31 | sendMode: 3,
32 | message: {
33 | info: {
34 | type: 'internal',
35 | ihrDisabled: false,
36 | bounce: true,
37 | bounced: false,
38 | dest: Address.parse('EQAZym3GBvem-frRGy1gUIaO-IBb5ByJPrm8aXtN7a_6PBW6'), // jetton-minter
39 | value: {
40 | coins: toNano('1') // ton amount
41 | },
42 | ihrFee: 0n,
43 | forwardFee: 0n,
44 | createdLt: 0n,
45 | createdAt: 0
46 | },
47 | body: beginCell()
48 | .storeUint(0x642b7d07, 32) // mint
49 | .storeUint(0, 64) // query_id
50 | .storeAddress(Address.parse('0QAR0lJjOVUzyT4QBKg50k216RBqvpvEPlq2_xGtdMkgFgcY')) // mint to this regular wallet
51 | .storeCoins(toNano('0.5')) // ton amount
52 | .storeRef(masterMsg)
53 | .endCell()
54 | }
55 | }],
56 | Math.floor(Date.now() / 1000 + 3600), // expired in hour
57 | toNano('1'), // ton amount
58 | 0, // index
59 | false, // not signer
60 | 123n // order_seqno
61 | );
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/tests/FeeComputation.spec.ts:
--------------------------------------------------------------------------------
1 | import { Blockchain, SandboxContract, TreasuryContract, prettyLogTransactions, BlockchainTransaction } from '@ton/sandbox';
2 | import { beginCell, Cell, internal, toNano, Transaction, storeAccountStorage, storeMessage, Slice, Message, Dictionary, storeStateInit, fromNano } from '@ton/core';
3 | import { Multisig, TransferRequest, Action, MultisigConfig } from '../wrappers/Multisig';
4 | import { Order } from '../wrappers/Order';
5 | import { Op } from '../wrappers/Constants';
6 | import '@ton/test-utils';
7 | import { compile } from '@ton/blueprint';
8 | import { randomAddress } from '@ton/test-utils';
9 | import { MsgPrices, getMsgPrices, collectCellStats, computeFwdFees, computeMessageForwardFees, shr16ceil, GasPrices, StorageValue, getGasPrices, computeGasFee, getStoragePrices, calcStorageFee, setMsgPrices, setGasPrice, setStoragePrices } from '../gasUtils';
10 | import { getRandomInt } from './utils';
11 |
12 | export const computedGeneric = (trans:Transaction) => {
13 | if(trans.description.type !== "generic")
14 | throw("Expected generic transaction");
15 | if(trans.description.computePhase.type !== "vm")
16 | throw("Compute phase expected")
17 | return trans.description.computePhase;
18 | };
19 | export const storageGeneric = (trans:Transaction) => {
20 | if(trans.description.type !== "generic")
21 | throw("Expected generic transaction");
22 | if(trans.description.computePhase.type !== "vm")
23 | throw("Compute phase expected")
24 | return trans.description.storagePhase;
25 | };
26 |
27 | describe('FeeComputation', () => {
28 | let multisig_code: Cell;
29 | let order_code: Cell;
30 | let msgPrices: MsgPrices;
31 |
32 | let curTime : () => number;
33 |
34 | beforeAll(async () => {
35 | multisig_code = await compile('Multisig');
36 | order_code = await compile('Order');
37 | });
38 |
39 | let blockchain: Blockchain;
40 | let multisigWallet: SandboxContract;
41 | let multisigJumbo: SandboxContract; // With 255 signers
42 | let deployer : SandboxContract;
43 | let second : SandboxContract;
44 | let proposer : SandboxContract;
45 | let signers : Array>;
46 |
47 | let testOrderEstimate: (wallet: SandboxContract, actions: Action[],
48 | time: number, gas_price?: GasPrices, fwd_price?: MsgPrices,
49 | storage_price?: StorageValue) => Promise;
50 |
51 | beforeEach(async () => {
52 | blockchain = await Blockchain.create();
53 |
54 | const _libs = Dictionary.empty(Dictionary.Keys.BigUint(256), Dictionary.Values.Cell());
55 | let code_raw = await compile('Order');
56 | _libs.set(BigInt(`0x${(await compile('Order')).hash().toString('hex')}`), code_raw);
57 | const libs = beginCell().storeDictDirect(_libs).endCell();
58 | blockchain.libs = libs;
59 |
60 | deployer = await blockchain.treasury('deployer');
61 | second = await blockchain.treasury('second');
62 | proposer = await blockchain.treasury('proposer');
63 |
64 | // Total max 255 signers
65 | signers = [deployer, second, ...await blockchain.createWallets(253)];
66 |
67 | let config = {
68 | threshold: 2,
69 | signers: [deployer.address, second.address],
70 | proposers: [proposer.address],
71 | allowArbitrarySeqno: false,
72 | };
73 | let jumboConfig = {
74 | threshold: 2,
75 | signers: signers.map(s => s.address),
76 | proposers: [proposer.address],
77 | allowArbitrarySeqno: false,
78 | }
79 |
80 | multisigWallet = blockchain.openContract(Multisig.createFromConfig(config, multisig_code));
81 | multisigJumbo = blockchain.openContract(Multisig.createFromConfig(jumboConfig, multisig_code));
82 |
83 | await multisigJumbo.sendDeploy(deployer.getSender(), toNano('100'));
84 | msgPrices = getMsgPrices(blockchain.config, 0);
85 |
86 | const deployResult = await multisigWallet.sendDeploy(deployer.getSender(), toNano('1'));
87 |
88 | curTime = () => blockchain.now ?? Math.floor(Date.now() / 1000);
89 | // Stop ticking
90 | blockchain.now = curTime();
91 |
92 | testOrderEstimate = async (wallet, actions, time, gas_price, fwd_price, storage_price) => {
93 | const gasPrices = gas_price || getGasPrices(blockchain.config, 0);
94 | const curMsgPrices = fwd_price || msgPrices;
95 | const curStoragePrices = storage_price || getStoragePrices(blockchain.config);
96 | const expTime = curTime() + time;
97 | let orderEstimateOnContract = await wallet.getOrderEstimate(actions, BigInt(expTime));
98 | const res = await wallet.sendNewOrder(deployer.getSender(), actions, expTime, orderEstimateOnContract);
99 |
100 | /*
101 | tx0 : external -> treasury
102 | tx1: treasury -> multisig
103 | tx2: multisig -> order
104 | */
105 |
106 | let MULTISIG_INIT_ORDER_GAS = computedGeneric(res.transactions[1]).gasUsed;
107 | let MULTISIG_INIT_ORDER_FEE = computedGeneric(res.transactions[1]).gasFees;
108 | let ORDER_INIT_GAS = computedGeneric(res.transactions[2]).gasUsed;
109 | let ORDER_INIT_GAS_FEE = computedGeneric(res.transactions[2]).gasFees;
110 |
111 | let orderAddress = await wallet.getOrderAddress(0n);
112 | let order = blockchain.openContract(Order.createFromAddress(orderAddress));
113 | let orderBody = (await order.getOrderData()).order;
114 | let orderBodyStats = collectCellStats(orderBody!, []);
115 |
116 | let smc = await blockchain.getContract(orderAddress);
117 | let accountStorage = beginCell().store(storeAccountStorage(smc.account.account!.storage)).endCell();
118 | let orderAccountStorageStats = collectCellStats(accountStorage, []);
119 |
120 | let orderStateOverhead = orderAccountStorageStats.sub(orderBodyStats);
121 | // {bits: orderAccountStorageStats.bits - orderBodyStats.bits, cells: orderAccountStorageStats.cells - orderBodyStats.cells};
122 |
123 | let multisigToOrderMessage = res.transactions[2].inMessage!;
124 | let multisigToOrderMessageStats = computeMessageForwardFees(curMsgPrices, multisigToOrderMessage).stats;
125 | let initOrderStateOverhead = multisigToOrderMessageStats.sub(orderBodyStats);
126 | // {bits: multisigToOrderMessageStats.bits - orderBodyStats.bits, cells: multisigToOrderMessageStats.cells - orderBodyStats.cells};
127 |
128 | // console.log("initOrderStateOverhead", initOrderStateOverhead);
129 |
130 | const firstApproval = await order.sendApprove(deployer.getSender(), 0);
131 | blockchain.now = expTime;
132 | const secondApproval = await order.sendApprove(second.getSender(), 1);
133 |
134 | expect(secondApproval.transactions).toHaveTransaction({
135 | from: order.address,
136 | to: wallet.address,
137 | op: Op.multisig.execute,
138 | success: true,
139 | outMessagesCount: actions.length
140 | });
141 |
142 | /*
143 | tx0 : external -> treasury
144 | tx1: treasury -> order
145 | tx2: order -> treasury (approve)
146 | tx3: order -> multisig
147 | tx4+: multisig -> destination
148 | */
149 | let orderToMultiownerMessage = secondApproval.transactions[3].inMessage!;
150 | let orderToMultiownerMessageStats = computeMessageForwardFees(curMsgPrices, orderToMultiownerMessage).stats;
151 | // console.log("Order to multisig stats:", orderToMultiownerMessageStats);
152 | // console.log("Order body stats:", orderBodyStats);
153 | let orderToMultiownerMessageOverhead = orderToMultiownerMessageStats.sub(orderBodyStats);
154 | // {bits: orderToMultiownerMessageStats.bits - orderBodyStats.bits, cells: orderToMultiownerMessageStats.cells - orderBodyStats.cells};
155 |
156 |
157 | let ORDER_EXECUTE_GAS = computedGeneric(secondApproval.transactions[1]).gasUsed;
158 | let ORDER_EXECUTE_FEE = computedGeneric(secondApproval.transactions[1]).gasFees;
159 | let MULTISIG_EXECUTE_GAS = actions.length > 1 ? 7486n : computedGeneric(secondApproval.transactions[3]).gasUsed;
160 | let MULTISIG_EXECUTE_FEE = actions.length > 1 ? computeGasFee(gasPrices, MULTISIG_EXECUTE_GAS) : computedGeneric(secondApproval.transactions[3]).gasFees;
161 | // console.log("orderToMultiownerMessageOverhead", orderToMultiownerMessageOverhead);
162 |
163 | // collect data in one console.log
164 | console.log(`
165 | MULTISIG_INIT_ORDER_GAS: ${MULTISIG_INIT_ORDER_GAS}
166 | ORDER_INIT_GAS: ${ORDER_INIT_GAS}
167 | ORDER_EXECUTE_GAS: ${ORDER_EXECUTE_GAS} MULTISIG_EXECUTE_GAS: ${MULTISIG_EXECUTE_GAS}
168 | orderStateOverhead: ${orderStateOverhead}
169 | initOrderStateOverhead: ${initOrderStateOverhead}
170 | orderToMultiownerMessageOverhead: ${orderToMultiownerMessageOverhead}
171 | `);
172 |
173 | let gasEstimate = [MULTISIG_INIT_ORDER_GAS , ORDER_INIT_GAS , ORDER_EXECUTE_GAS , MULTISIG_EXECUTE_GAS].map(g => computeGasFee(gasPrices, g)).reduce((cur, acc) => acc + cur, 0n);
174 | let gasFees = MULTISIG_INIT_ORDER_FEE + ORDER_INIT_GAS_FEE + ORDER_EXECUTE_FEE + MULTISIG_EXECUTE_FEE;
175 | expect(gasFees).toEqual(gasEstimate);
176 | // expect(gasFees).toEqual(orderEstimateOnContract.gas);
177 |
178 | // blockchain.verbosity = {vmLogs:"vm_logs_verbose", print: true, debugLogs: true, blockchainLogs: true};
179 | let actualFwd = computeFwdFees(curMsgPrices, orderToMultiownerMessageStats.cells, orderToMultiownerMessageStats.bits) +
180 | computeFwdFees(curMsgPrices, multisigToOrderMessageStats.cells, multisigToOrderMessageStats.bits);
181 |
182 | let storageEstimate = calcStorageFee(curStoragePrices, orderBodyStats.add(orderStateOverhead), BigInt(time));//shr16ceil((orderBodyStats.bits +orderStateOverhead.bits + (orderBodyStats.cells + orderStateOverhead.cells) * 500n) * BigInt(time));
183 | const storagePhase = storageGeneric(secondApproval.transactions[1]);
184 | const actualStorage = storagePhase?.storageFeesCollected;
185 | console.log("Storage estimates:", storageEstimate, actualStorage);
186 | expect(storageEstimate).toEqual(actualStorage);
187 | // expect(storageEstimate).toEqual(orderEstimateOnContract.storage);
188 | let manualFees = gasEstimate + actualFwd + storageEstimate;
189 | console.log("orderEstimates", orderEstimateOnContract, manualFees);
190 | // It's ok if contract overestimates fees a little
191 | expect(manualFees).toBeLessThanOrEqual(orderEstimateOnContract);
192 | const feesDelta = orderEstimateOnContract - manualFees;
193 | if(feesDelta > 0) {
194 | console.log("Contract overestimated fees by:", fromNano(feesDelta));
195 | }
196 | }
197 | });
198 |
199 | it('should send new order with contract estimated message value', async () => {
200 | const testAddr = randomAddress();
201 | const testMsg: TransferRequest = {type:"transfer", sendMode: 1, message: internal({to: testAddr, value: toNano('0.015'), body: beginCell().storeUint(12345, 32).endCell()})};
202 | const orderList:Array = [testMsg];
203 | let timeSpan = 365 * 24 * 3600;
204 | await testOrderEstimate(multisigWallet, orderList, timeSpan);
205 | });
206 | it('should estimate correctly with 255 signers multisig', async () => {
207 | const testAddr = randomAddress();
208 | const testMsg: TransferRequest = {type:"transfer", sendMode: 1, message: internal({to: testAddr, value: toNano('0.015'), body: beginCell().storeUint(12345, 32).endCell()})};
209 | const orderList:Array = [testMsg];
210 | let timeSpan = 365 * 24 * 3600;
211 | await testOrderEstimate(multisigJumbo, orderList, timeSpan);
212 | });
213 | it('should estimate correctly with large order', async () => {
214 | const orderList:Array = [];
215 | let timeSpan = 365 * 24 * 3600;
216 |
217 | for(let i = 0; i < 100; i++ ) {
218 | const testAddr = randomAddress();
219 | const testMsg: TransferRequest = {type:"transfer", sendMode: 1, message: internal({to: testAddr, value: toNano('0.015'), body: beginCell().storeUint(getRandomInt(10000, 20000), 32).endCell()})};
220 | orderList.push(testMsg);
221 | }
222 | await testOrderEstimate(multisigJumbo, orderList, timeSpan);
223 | });
224 | it('should estimate correctly on gas fees change', async () => {
225 | const testAddr = randomAddress();
226 | const testMsg: TransferRequest = {type:"transfer", sendMode: 1, message: internal({to: testAddr, value: toNano('0.015'), body: beginCell().storeUint(12345, 32).endCell()})};
227 | const orderList:Array = [testMsg];
228 | let timeSpan = 365 * 24 * 3600;
229 | const storeTill = BigInt(curTime() + timeSpan);
230 | const oldConfig = blockchain.config;
231 |
232 | const gasPrices = getGasPrices(oldConfig, 0);
233 | const newPrices: GasPrices = {
234 | ...gasPrices,
235 | gas_price: gasPrices.gas_price * 3n
236 | };
237 |
238 | //blockchain.verbosity = {vmLogs:"vm_logs_verbose", print: true, debugLogs: true, blockchainLogs: true};
239 | const estBefore = await multisigJumbo.getOrderEstimate(orderList, storeTill);
240 | blockchain.setConfig(
241 | setGasPrice(blockchain.config, newPrices, 0)
242 | );
243 | const estAfter = await multisigJumbo.getOrderEstimate(orderList, storeTill);
244 | expect(estAfter).toBeGreaterThan(estBefore);
245 |
246 | await testOrderEstimate(multisigJumbo, orderList, timeSpan, newPrices);
247 |
248 | blockchain.setConfig(oldConfig);
249 | });
250 | it('should estimate correctly on forward fees change', async () => {
251 | const testAddr = randomAddress();
252 | const testMsg: TransferRequest = {type:"transfer", sendMode: 1, message: internal({to: testAddr, value: toNano('0.015'), body: beginCell().storeUint(12345, 32).endCell()})};
253 | const orderList:Array = [testMsg];
254 | let timeSpan = 365 * 24 * 3600;
255 | const storeTill = BigInt(curTime() + timeSpan);
256 | const oldConfig = blockchain.config;
257 | const newPrices : MsgPrices = {
258 | ...msgPrices,
259 | bitPrice: msgPrices.bitPrice * 10n,
260 | cellPrice: msgPrices.cellPrice * 10n
261 | }
262 |
263 | const estBefore = await multisigJumbo.getOrderEstimate(orderList, storeTill);
264 |
265 | blockchain.setConfig(setMsgPrices(blockchain.config, newPrices, 0));
266 |
267 | const estAfter = await multisigJumbo.getOrderEstimate(orderList, storeTill);
268 | expect(estAfter).toBeGreaterThan(estBefore);
269 |
270 | await testOrderEstimate(multisigJumbo, orderList, timeSpan, undefined, newPrices);
271 |
272 | blockchain.setConfig(oldConfig);
273 | });
274 | it('should estimate correctly on storage fees change', async () => {
275 | const testAddr = randomAddress();
276 | const testMsg: TransferRequest = {type:"transfer", sendMode: 1, message: internal({to: testAddr, value: toNano('0.015'), body: beginCell().storeUint(12345, 32).endCell()})};
277 | const orderList:Array = [testMsg];
278 | let timeSpan = 365 * 24 * 3600;
279 | const storeTill = BigInt(curTime() + timeSpan);
280 | const oldConfig = blockchain.config;
281 |
282 | const storagePrices = getStoragePrices(blockchain.config);
283 | const newPrices: StorageValue = {
284 | ...storagePrices,
285 | cell_price_ps: storagePrices.cell_price_ps * 10n,
286 | bit_price_ps: storagePrices.bit_price_ps * 10n
287 | };
288 |
289 | const estBefore = await multisigJumbo.getOrderEstimate(orderList, storeTill);
290 |
291 | blockchain.setConfig(setStoragePrices(oldConfig, newPrices));
292 | const estAfter = await multisigJumbo.getOrderEstimate(orderList, storeTill);
293 | expect(estAfter).toBeGreaterThan(estBefore);
294 | await testOrderEstimate(multisigJumbo, orderList, timeSpan, undefined, undefined, newPrices);
295 |
296 | blockchain.setConfig(oldConfig);
297 | });
298 |
299 | it('common cases gas fees multisig', async () => {
300 | const assertMultisig = async (threshold: number, total: number, txcount: number, lifetime: number, signer_creates: boolean) => {
301 | let totalGas = 0n;
302 | const testWallet = await blockchain.treasury('test_wallet'); // Make sure we don't bounce
303 | const signers = await blockchain.createWallets(total);
304 | const config : MultisigConfig = {
305 | threshold,
306 | signers : signers.map(x => x.address),
307 | proposers: [proposer.address],
308 | allowArbitrarySeqno: false
309 | };
310 |
311 | const multisig = blockchain.openContract(Multisig.createFromConfig(config, multisig_code));
312 |
313 |
314 | const creator = signer_creates ? signers[0] : proposer;
315 | const signerIdx = signer_creates ? 1 : 0;
316 | let res = await multisig.sendDeploy(deployer.getSender(), toNano('10'));
317 | expect(res.transactions).toHaveTransaction({
318 | from: deployer.address,
319 | to: multisig.address,
320 | deploy: true,
321 | success: true
322 | });
323 |
324 | const dataBefore = await multisig.getMultisigData();
325 | const initSeqno = dataBefore.nextOrderSeqno;
326 | const orderContract = blockchain.openContract(Order.createFromAddress(await multisig.getOrderAddress(initSeqno)));
327 |
328 | blockchain.now = curTime();
329 | const testMsg: TransferRequest = {type:"transfer", sendMode: 1, message: internal({to: testWallet.address, value: toNano('0.015'), body: beginCell().storeUint(12345, 32).endCell()})};
330 | const actions: Array = [];
331 | for (let i = 0; i < txcount; i++) {
332 | actions.push(testMsg);
333 | }
334 | res = await multisig.sendNewOrder(creator.getSender(), actions, blockchain.now + lifetime);
335 | expect(res.transactions).toHaveTransaction({
336 | from: creator.address,
337 | to: multisig.address,
338 | success: true
339 | });
340 | expect(res.transactions).toHaveTransaction({
341 | from: multisig.address,
342 | to: orderContract.address,
343 | deploy: true,
344 | success: true
345 | });
346 |
347 | const summTx = (summ: bigint, tx: BlockchainTransaction) => summ + tx.totalFees.coins;
348 | totalGas += res.transactions.reduce(summTx, 0n);
349 |
350 | blockchain.now += lifetime;
351 | for (let i = signerIdx; i < threshold; i++ ) {
352 | res = await orderContract.sendApprove(signers[i].getSender(), i);
353 | totalGas += res.transactions.reduce(summTx, 0n);
354 | }
355 | expect(res.transactions).toHaveTransaction({
356 | from: orderContract.address,
357 | to: multisig.address,
358 | op: Op.multisig.execute,
359 | success: true
360 | });
361 | expect(res.transactions).toHaveTransaction({
362 | from: multisig.address,
363 | to: testWallet.address,
364 | success: true
365 | });
366 | expect(res.transactions).not.toHaveTransaction({
367 | actionResultCode: (x) => x! != 0
368 | });
369 |
370 |
371 | return totalGas;
372 | };
373 |
374 |
375 | const week = 3600 * 24 * 7;
376 | const gas1 = await assertMultisig(7, 10, 1, week, false);
377 | const gas2 = await assertMultisig(2, 3, 1, week, true);
378 | const gas3 = await assertMultisig(1, 3, 1, week, true);
379 | const gas4 = await assertMultisig(7, 10, 100, week, false);
380 |
381 | console.log("Multisig 7/10 1 transfer 1 week proposer:", gas1);
382 | console.log("Multisig 2/3 1 transfer 1 week signer:", gas2);
383 | console.log("Multisig 1/3 1 transfer 1 week signer:", gas3);
384 | console.log("Multisig 7/10 100 transfer 1 week proposer:", gas4);
385 | });
386 |
387 | it('should be enough for 75 years', async () => {
388 | const testAddr = randomAddress();
389 | const testMsg: TransferRequest = {type:"transfer", sendMode: 1, message: internal({to: testAddr, value: toNano('0.015'), body: beginCell().storeUint(12345, 32).endCell()})};
390 | const testMsg2: TransferRequest = {type:"transfer", sendMode: 1, message: internal({to: randomAddress(), value: toNano('0.017'), body: beginCell().storeUint(123425, 32).endCell()})};
391 | const orderList:Array = [testMsg];
392 | let timeSpan = 365 * 24 * 3600 * 75;
393 | const expTime = Math.floor(Date.now() / 1000) + timeSpan;
394 | let orderEstimateOnContract = await multisigWallet.getOrderEstimate(orderList, BigInt(expTime));
395 | const res = await multisigWallet.sendNewOrder(deployer.getSender(), orderList, expTime, orderEstimateOnContract + 1n);
396 |
397 | let orderAddress = await multisigWallet.getOrderAddress(0n);
398 | let order = blockchain.openContract(Order.createFromAddress(orderAddress));
399 |
400 | expect(res.transactions).toHaveTransaction({
401 | from: multisigWallet.address,
402 | to: order.address,
403 | success: true,
404 | });
405 |
406 | const firstApproval = await order.sendApprove(deployer.getSender(), 0);
407 | blockchain.now = expTime - 10;
408 | const secondApproval = await order.sendApprove(second.getSender(), 1);
409 |
410 | expect(secondApproval.transactions).toHaveTransaction({
411 | from: order.address,
412 | to: multisigWallet.address
413 | });
414 |
415 | const storagePhase = storageGeneric(secondApproval.transactions[1]);
416 | const actualStorage = storagePhase?.storageFeesCollected;
417 | console.log("Storage estimates:", actualStorage);
418 | });
419 |
420 | });
421 |
--------------------------------------------------------------------------------
/tests/Order.spec.ts:
--------------------------------------------------------------------------------
1 | import { Address, beginCell, Cell, internal as internal_relaxed, toNano, Transaction, Dictionary } from '@ton/core';
2 | import { Order, OrderConfig } from '../wrappers/Order';
3 | import { Op, Errors, Params } from "../wrappers/Constants";
4 | import '@ton/test-utils';
5 | import { compile } from '@ton/blueprint';
6 | import { findTransactionRequired, randomAddress } from '@ton/test-utils';
7 | import { Blockchain, BlockchainSnapshot, SandboxContract, TreasuryContract, internal } from '@ton/sandbox';
8 | import { differentAddress, getMsgPrices, getRandomInt, storageCollected, computedGeneric } from './utils';
9 | import { Multisig, TransferRequest } from '../wrappers/Multisig';
10 |
11 | type ApproveResponse = {
12 | status: number,
13 | query_id: bigint,
14 | exit_code?: number
15 | };
16 |
17 | describe('Order', () => {
18 | let code: Cell;
19 | let blockchain: Blockchain;
20 | let threshold: number;
21 | let orderContract : SandboxContract;
22 | let mockOrder: Cell;
23 | let multisig : SandboxContract;
24 | let signers: Array>;
25 | let notSigner: SandboxContract;
26 | let prevState: BlockchainSnapshot;
27 | let prices : ReturnType;
28 | let getContractData : (addr: Address) => Promise;
29 |
30 | let testPartial : (cmp: any, match: any) => boolean;
31 | let testApproveResponse : (body: Cell, match:Partial) => boolean;
32 | let testApprove: (txs: Transaction[], from: Address, to: Address,
33 | exp: number, query_id?: number | bigint) => void;
34 |
35 | beforeAll(async () => {
36 | let code_raw = await compile('Order');
37 | blockchain = await Blockchain.create();
38 |
39 | const _libs = Dictionary.empty(Dictionary.Keys.BigUint(256), Dictionary.Values.Cell());
40 | _libs.set(BigInt(`0x${code_raw.hash().toString('hex')}`), code_raw);
41 | const libs = beginCell().storeDictDirect(_libs).endCell();
42 | blockchain.libs = libs;
43 | let lib_prep = beginCell().storeUint(2,8).storeBuffer(code_raw.hash()).endCell();
44 | code = new Cell({ exotic:true, bits: lib_prep.bits, refs:lib_prep.refs});
45 |
46 |
47 | multisig = await blockchain.treasury('multisig');
48 | notSigner = await blockchain.treasury('notSigner');
49 | const testAddr = randomAddress();
50 | const testMsg : TransferRequest = { type: "transfer", sendMode: 1, message: internal_relaxed({to: testAddr, value: toNano('0.015'), body: beginCell().storeUint(12345, 32).endCell()})};
51 |
52 | mockOrder = Multisig.packOrder([testMsg]);
53 |
54 | orderContract = blockchain.openContract(Order.createFromConfig({
55 | multisig: multisig.address,
56 | orderSeqno: 0
57 | }, code));
58 |
59 | prices = getMsgPrices(blockchain.config, 0);
60 |
61 | getContractData = async (address: Address) => {
62 | const smc = await blockchain.getContract(address);
63 | if(!smc.account.account)
64 | throw("Account not found")
65 | if(smc.account.account.storage.state.type != "active" )
66 | throw("Atempting to get data on inactive account");
67 | if(!smc.account.account.storage.state.state.data)
68 | throw("Data is not present");
69 | return smc.account.account.storage.state.state.data
70 | }
71 | testPartial = (cmp: any, match: any) => {
72 | for (let key in match) {
73 | if(!(key in cmp)) {
74 | throw Error(`Unknown key ${key} in ${cmp}`);
75 | }
76 |
77 | if(match[key] instanceof Address) {
78 | if(!(cmp[key] instanceof Address)) {
79 | return false
80 | }
81 | if(!(match[key] as Address).equals(cmp[key])) {
82 | return false
83 | }
84 | }
85 | else if(match[key] instanceof Cell) {
86 | if(!(cmp[key] instanceof Cell)) {
87 | return false;
88 | }
89 | if(!(match[key] as Cell).equals(cmp[key])) {
90 | return false;
91 | }
92 | }
93 | else if(match[key] !== cmp[key]){
94 | return false;
95 | }
96 | }
97 | return true;
98 | }
99 | testApproveResponse = (body, match) => {
100 | let exitCode: number;
101 | const ds = body.beginParse();
102 | const approveStatus = ds.loadUint(32);
103 | const cmp: ApproveResponse = {
104 | status: approveStatus,
105 | query_id: ds.loadUintBig(64),
106 | exit_code: approveStatus == Op.order.approve_rejected ? ds.loadUint(32) : undefined
107 | };
108 | return testPartial(cmp, match);
109 | }
110 | testApprove = (txs, from, on, exp) => {
111 | let expStatus: number;
112 | let exitCode: number | undefined;
113 |
114 | const approveTx = findTransactionRequired(txs, {
115 | from,
116 | on,
117 | op: Op.order.approve,
118 | success: true,
119 | outMessagesCount: (x) => x >= 1
120 | });
121 | const inMsg = approveTx.inMessage!;
122 | if(inMsg.info.type !== "internal")
123 | throw new Error("Can't be");
124 |
125 | const inQueryId = inMsg.body.beginParse().skip(32).preloadUintBig(64);
126 | const inValue = inMsg.info.value;
127 |
128 | if(exp == 0) {
129 | expStatus = Op.order.approved;
130 | exitCode = undefined;
131 | }
132 | else {
133 | expStatus = Op.order.approve_rejected;
134 | exitCode = exp;
135 | }
136 | expect(txs).toHaveTransaction({
137 | // Response message
138 | from: on,
139 | on: from,
140 | body: (x) => testApproveResponse(x!, {
141 | status: expStatus,
142 | query_id: inQueryId,
143 | exit_code: exitCode
144 | }),
145 | value: inValue.coins - prices.lumpPrice - computedGeneric(approveTx).gasFees
146 | })
147 | }
148 |
149 |
150 | blockchain.now = Math.floor(Date.now() / 1000);
151 | const expDate = blockchain.now + 1000;
152 |
153 | threshold = 5
154 | signers = await blockchain.createWallets(threshold * 2);
155 | const res = await orderContract.sendDeploy(multisig.getSender(), toNano('1'), signers.map((s) => s.address), expDate, mockOrder, threshold);
156 | expect(res.transactions).toHaveTransaction({deploy: true, success: true});
157 |
158 | const stringify = (addr: Address) => addr.toString();
159 | const orderData = await orderContract.getOrderData();
160 |
161 | // Overlaps with "deployed order state should match requested" case from Multisig.spec.ts but won't hurt
162 | expect(orderData.multisig).toEqualAddress(multisig.address);
163 | expect(orderData.order_seqno).toBe(0n);
164 | expect(orderData.expiration_date).toEqual(BigInt(expDate));
165 | expect(orderData.approvals_num).toBe(0); // Number of approvals
166 | expect(orderData._approvals).toBe(0n); // Approvals raw bitmask
167 | expect(orderData.signers.map(stringify)).toEqual(signers.map(s => stringify(s.address)));
168 | expect(orderData.threshold).toBe(5);
169 | expect(orderData.executed).toBe(false);
170 | expect(orderData.order).toEqualCell(mockOrder);
171 |
172 | prevState = blockchain.snapshot();
173 | });
174 |
175 | afterEach(async () => await blockchain.loadFrom(prevState));
176 |
177 | it('should deploy', async () => {
178 | // Happens in beforeAll clause
179 | });
180 |
181 | it('should only accept init message from multisig', async () => {
182 |
183 | const newOrder = blockchain.openContract(Order.createFromConfig({
184 | multisig: multisig.address,
185 | orderSeqno: 1234 // Next
186 | }, code));
187 |
188 | const expDate = blockchain.now! + 1000;
189 |
190 | const testSender = await blockchain.treasury('totally_not_multisig');
191 | let res = await newOrder.sendDeploy(testSender.getSender(), toNano('1'), signers.map(s => s.address), expDate, mockOrder, threshold);
192 |
193 | expect(res.transactions).toHaveTransaction({
194 | from: testSender.address,
195 | to: newOrder.address,
196 | success: false,
197 | aborted: true,
198 | exitCode: Errors.order.unauthorized_init
199 | });
200 |
201 | // Now retry with legit multisig should succeed
202 | const dataBefore = await newOrder.getOrderData();
203 | expect(dataBefore.inited).toBe(false);
204 | expect(dataBefore.threshold).toBe(null);
205 |
206 | res = await newOrder.sendDeploy(multisig.getSender(), toNano('1'), signers.map(s => s.address), expDate, mockOrder, threshold);
207 |
208 | expect(res.transactions).toHaveTransaction({
209 | from: multisig.address,
210 | to: newOrder.address,
211 | success: true,
212 | });
213 |
214 | const dataAfter = await newOrder.getOrderData();
215 | expect(dataAfter.inited).toBe(true);
216 | expect(dataAfter.threshold).toEqual(threshold);
217 | });
218 | it('should reject already expired init message', async () => {
219 | const newOrder = blockchain.openContract(Order.createFromConfig({
220 | multisig: multisig.address,
221 | orderSeqno: 123 // Next
222 | }, code));
223 | const expDate = blockchain.now! - 1;
224 |
225 | let res = await newOrder.sendDeploy(multisig.getSender(), toNano('1'), signers.map(s => s.address), expDate, mockOrder, threshold);
226 |
227 | expect(res.transactions).toHaveTransaction({
228 | from: multisig.address,
229 | on: newOrder.address,
230 | op: Op.order.init,
231 | success: false,
232 | aborted: true,
233 | deploy: true,
234 | exitCode: Errors.order.expired
235 | });
236 |
237 | const dataBefore = await newOrder.getOrderData();
238 | expect(dataBefore.inited).toBe(false);
239 | expect(dataBefore.threshold).toBe(null);
240 |
241 | // now == expiration_date should be allowed (currently not allowed).
242 | res = await newOrder.sendDeploy(multisig.getSender(), toNano('1'), signers.map(s => s.address), blockchain.now!, mockOrder, threshold);
243 |
244 | expect(res.transactions).toHaveTransaction({
245 | from: multisig.address,
246 | on: newOrder.address,
247 | op: Op.order.init,
248 | success: true,
249 | });
250 |
251 | const dataAfter = await newOrder.getOrderData();
252 | expect(dataAfter.inited).toBe(true);
253 | expect(dataAfter.threshold).toEqual(threshold);
254 | });
255 | it('order contract should accept init message only once if approve_on_init = false', async () => {
256 | const expDate = Number((await orderContract.getOrderData()).expiration_date);
257 | const dataBefore = await getContractData(orderContract.address);
258 | const approveInit = false;
259 |
260 | const res = await orderContract.sendDeploy(multisig.getSender(), toNano('1'), signers.map(s => s.address), expDate, mockOrder, threshold, approveInit);
261 |
262 | expect(res.transactions).toHaveTransaction({
263 | from: multisig.address,
264 | to: orderContract.address,
265 | success: false,
266 | aborted: true,
267 | exitCode: Errors.order.already_inited
268 | });
269 |
270 | // To be extra sure that there is no commit()
271 | expect(dataBefore).toEqualCell(await getContractData(orderContract.address));
272 | });
273 | it('order contract should reject secondary init vote if any of order info has changed', async() => {
274 | const approveOnInit = true;
275 | const idx = 0;
276 | const newSigners = (await blockchain.createWallets(10)).map(s => s.address);
277 | const curSigners = signers.map(s => s.address);
278 | const stateBefore = await getContractData(orderContract.address);
279 | const expInited = Errors.order.already_inited;
280 | const expSuccess = 0;
281 | const dataBefore = await orderContract.getOrderData();
282 | const expDate = Number(dataBefore.expiration_date);
283 |
284 | let testInit = async (signers: Address[], expDate : number, order: Cell, threshold: number, exp: number) => {;
285 | const res = await orderContract.sendDeploy(multisig.getSender(), toNano('1'), signers, expDate, order, threshold, approveOnInit, idx);
286 | expect(res.transactions).toHaveTransaction({
287 | on: orderContract.address,
288 | from: multisig.address,
289 | success: exp == 0,
290 | aborted: exp != 0,
291 | exitCode: exp
292 | });
293 | if(exp == 0) {
294 | const dataAfter = await orderContract.getOrderData();
295 | expect(dataAfter.approvals_num).toEqual(Number(dataBefore.approvals_num) + 1);
296 | expect(dataAfter._approvals).toBeGreaterThan(dataBefore._approvals ?? 0n);
297 | expect(dataAfter.approvals[idx]).toBe(true);
298 | }
299 | else {
300 | expect(await getContractData(orderContract.address)).toEqualCell(stateBefore);
301 | }
302 | }
303 | // Change signers
304 | await testInit(newSigners, expDate, mockOrder, threshold, expInited);
305 | // Change expDate
306 | await testInit(curSigners, expDate + getRandomInt(100, 200), mockOrder, threshold, expInited);
307 | // Change order
308 | const testMsg : TransferRequest = { type: "transfer", sendMode: 1, message: internal_relaxed({to: randomAddress(0), value: ('0.015'), body: beginCell().storeUint(getRandomInt(100000, 200000), 32).endCell()})};
309 | const newOrder = Multisig.packOrder([testMsg]);
310 |
311 | expect(newOrder).not.toEqualCell(mockOrder);
312 |
313 | await testInit(curSigners, expDate, newOrder, threshold, expInited);
314 | // Change threshold
315 | await testInit(curSigners, expDate, mockOrder, threshold + getRandomInt(10, 20), expInited);
316 | // Expect success
317 | await testInit(curSigners, expDate, mockOrder, threshold, expSuccess);
318 | });
319 | it('order contract should treat multiple init messages as votes if approve_on_init = true', async () => {
320 | const approveOnInit = true;
321 | for(let i = 0; i < threshold; i++) {
322 | const dataBefore = await orderContract.getOrderData();
323 | const expDate = Number(dataBefore.expiration_date);
324 | const res = await orderContract.sendDeploy(multisig.getSender(), toNano('1'), signers.map(s => s.address), expDate, mockOrder, threshold, approveOnInit, i);
325 |
326 | expect(res.transactions).toHaveTransaction({
327 | on: orderContract.address,
328 | from: multisig.address,
329 | op: Op.order.init,
330 | success: true
331 | });
332 | const dataAfter = await orderContract.getOrderData();
333 |
334 | expect(dataAfter.approvals_num).toEqual(i + 1);
335 | expect(dataAfter.approvals[i]).toBe(true);
336 | if(dataBefore.approvals === null) {
337 | expect(dataAfter._approvals).not.toBe(null);
338 | }
339 | else {
340 | expect(dataAfter._approvals).toBeGreaterThan(dataBefore._approvals!);
341 | }
342 |
343 | if(i + 1 == threshold) {
344 | expect(res.transactions).toHaveTransaction({
345 | on: multisig.address,
346 | from: orderContract.address,
347 | op: Op.multisig.execute
348 | });
349 | }
350 | }
351 | })
352 | it('should not be possible to use multiple init msg to approve multiple idx twice', async () => {
353 | const expDate = Number((await orderContract.getOrderData()).expiration_date);
354 | const signerIdx = getRandomInt(0, signers.length - 1);
355 |
356 | const approveOnInit = true;
357 |
358 | let res = await orderContract.sendDeploy(multisig.getSender(), toNano('1'), signers.map(s => s.address), expDate, mockOrder, threshold, approveOnInit, signerIdx);
359 |
360 | expect(res.transactions).toHaveTransaction({
361 | on: orderContract.address,
362 | from: multisig.address,
363 | op: Op.order.init,
364 | success: true
365 | });
366 |
367 | let dataInit = await orderContract.getOrderData();
368 | expect(dataInit.approvals_num).toEqual(1);
369 | expect(dataInit.approvals[signerIdx]).toBe(true);
370 |
371 | let dataBefore = await getContractData(orderContract.address);
372 | // Repeat init
373 | res = await orderContract.sendDeploy(multisig.getSender(), toNano('1'), signers.map(s => s.address), expDate, mockOrder, threshold, approveOnInit, signerIdx);
374 |
375 | expect(res.transactions).toHaveTransaction({
376 | from: orderContract.address,
377 | // to: signers[signerIdx].address ?
378 | body: (x) => testApproveResponse(x!, {
379 | status: Op.order.approve_rejected,
380 | exit_code: Errors.order.already_approved
381 | })
382 | });
383 |
384 | expect(await getContractData(orderContract.address)).toEqualCell(dataBefore);
385 | });
386 |
387 | it('should approve order', async () => {
388 | const idxMap = Array.from(signers.keys());
389 | let idxCount = idxMap.length - 1;
390 | for (let i = 0; i < threshold; i++) {
391 | let signerIdx: number;
392 | if(idxCount > 1) {
393 | // Removing used index
394 | signerIdx = idxMap.splice(getRandomInt(0, idxCount), 1)[0];
395 | idxCount--;
396 | }
397 | else {
398 | signerIdx = 0;
399 | }
400 | const signerWallet = signers[signerIdx];
401 | const res = await orderContract.sendApprove(signerWallet.getSender(), signerIdx);
402 | const thresholdHit = i == threshold - 1;
403 |
404 | testApprove(res.transactions, signerWallet.address, orderContract.address, 0);
405 |
406 | const orderData = await orderContract.getOrderData();
407 |
408 | expect(orderData.approvals_num).toEqual(i + 1);
409 | expect(orderData.approvals[signerIdx]).toBe(true);
410 | expect(orderData.executed).toEqual(thresholdHit);
411 |
412 | if(thresholdHit) {
413 | expect(res.transactions).toHaveTransaction({
414 | from: orderContract.address,
415 | to: multisig.address,
416 | op: Op.multisig.execute
417 | });
418 | }
419 | else {
420 | expect(res.transactions).not.toHaveTransaction({
421 | from: orderContract.address,
422 | to: multisig.address,
423 | op: Op.multisig.execute
424 | });
425 | }
426 | }
427 | });
428 |
429 | it('should approve order with comment', async () => {
430 | let testOrderComment = async (payload: Cell, exp: number) => {
431 | const rollBack = blockchain.snapshot();
432 |
433 | const dataBefore = await orderContract.getOrderData();
434 | const rndIdx = getRandomInt(0, signers.length - 1);
435 | const signer = signers[rndIdx];
436 | const expSuccess = exp == 0;
437 | expect(Number(dataBefore.approvals_num)).toBe(0);
438 |
439 |
440 | const res = await blockchain.sendMessage(internal({
441 | from: signer.address,
442 | to: orderContract.address,
443 | value: toNano('1'),
444 | body: payload
445 | }));
446 |
447 | const dataAfter = await orderContract.getOrderData();
448 |
449 | if(expSuccess) {
450 | expect(res.transactions).toHaveTransaction({
451 | from: orderContract.address,
452 | to: signer.address,
453 | op: Op.order.approved,
454 | success: expSuccess,
455 | aborted: false,
456 | exitCode: 0
457 | });
458 |
459 | expect(dataAfter.approvals_num).toEqual(Number(dataBefore.approvals_num) + 1);
460 | expect(dataAfter._approvals).toBeGreaterThan(dataBefore._approvals ?? 0n);
461 | expect(dataAfter.approvals[rndIdx]).toBe(true);
462 | }
463 | else {
464 | expect(res.transactions).toHaveTransaction({
465 | from: signer.address,
466 | on: orderContract.address,
467 | success: false,
468 | aborted: true,
469 | exitCode: exp
470 | });
471 | expect(dataAfter.approvals_num).toEqual(dataBefore.approvals_num);
472 | expect(dataAfter._approvals).toEqual(dataBefore._approvals);
473 | expect(dataAfter.approvals[rndIdx]).toBe(false);
474 | }
475 | await blockchain.loadFrom(rollBack);
476 | };
477 |
478 | let approveStr = "approve";
479 |
480 | // Plain approve should succeed
481 | await testOrderComment(beginCell().storeUint(0, 32)
482 | .storeStringTail(approveStr).endCell(), 0);
483 | // Start wit ref should succeed
484 | await testOrderComment(beginCell().storeUint(0, 32)
485 | .storeStringRefTail(approveStr).endCell(), 0);
486 |
487 | // Each letter in separate ref
488 | let joinedStr: Cell | undefined;
489 | let lettersLeft = approveStr.length - 1;
490 |
491 | do {
492 | const chunk = beginCell().storeStringTail(approveStr[lettersLeft]);
493 | if(joinedStr == undefined) {
494 | joinedStr = chunk.endCell();
495 | }
496 | else {
497 | joinedStr = chunk.storeRef(joinedStr).endCell();
498 | }
499 | } while(lettersLeft--);
500 |
501 | expect(joinedStr.depth()).toEqual(approveStr.length - 1);
502 | await testOrderComment(beginCell().storeUint(0, 32)
503 | .storeSlice(joinedStr.beginParse()).endCell(), 0);
504 | // Tricky comment
505 | await testOrderComment(beginCell().storeUint(0, 32)
506 | .storeStringTail("approve").storeRef(
507 | beginCell().storeStringTail(" not given").endCell()
508 | ).endCell(), Errors.order.unknown_op)
509 | // Tricky positive case
510 | // Empty refs in between comment symbols shouldn't be a problem
511 | await testOrderComment(beginCell().storeUint(0, 32)
512 | .storeStringTail("ap")
513 | .storeRef(beginCell()
514 | .storeRef(
515 | beginCell()
516 | .storeStringRefTail("prove").endCell()
517 | ).endCell())
518 | .endCell(), 0)
519 |
520 | await testOrderComment(beginCell().storeUint(0, 32).storeStringTail("approve not given")
521 | .endCell(), Errors.order.unknown_op);
522 | });
523 |
524 |
525 | it('should reject order with comment from not signer', async () => {
526 | let signerIdx = 0;
527 | let signer = notSigner;
528 | let dataBefore = await orderContract.getOrderData();
529 | let res = await blockchain.sendMessage(internal({
530 | from: signer.address,
531 | to: orderContract.address,
532 | value: toNano('1'),
533 | body: beginCell().storeUint(0, 32).storeStringTail("approve").endCell()
534 | }));
535 |
536 | expect(res.transactions).toHaveTransaction({
537 | from: signer.address,
538 | to: orderContract.address,
539 | success: false,
540 | exitCode: Errors.order.unauthorized_sign
541 | });
542 | let dataAfter = await orderContract.getOrderData();
543 |
544 | // All should stay same
545 | expect(dataAfter.approvals_num).toEqual(dataBefore.approvals_num);
546 | expect(dataAfter._approvals).toEqual(dataBefore._approvals);
547 |
548 | });
549 | it('should accept approval only from signers', async () => {
550 | let signerIdx = getRandomInt(0, signers.length - 1);
551 |
552 | const rndSigner = signers[signerIdx];
553 | const notSigner = differentAddress(signers[signerIdx].address);
554 | const msgVal = toNano('0.1');
555 | // Query id match is important in that case
556 | const rndQueryId = BigInt(getRandomInt(1000, 2000));
557 |
558 | let dataBefore = await getContractData(orderContract.address);
559 |
560 | // Testing not valid signer address, but valid signer index
561 | let res = await orderContract.sendApprove(blockchain.sender(notSigner), signerIdx, msgVal, rndQueryId);
562 | expect(res.transactions).toHaveTransaction({
563 | on: orderContract.address,
564 | from: notSigner,
565 | op: Op.order.approve,
566 | success: false,
567 | aborted: true,
568 | exitCode: Errors.order.unauthorized_sign
569 | });
570 |
571 | // testApprove(res.transactions, notSigner, orderContract.address, Errors.order.unauthorized_sign);
572 |
573 | expect(await getContractData(orderContract.address)).toEqualCell(dataBefore);
574 |
575 | // Now let's pick valid signer address but index from another valid signer
576 |
577 | signerIdx = (signerIdx + 1) % signers.length;
578 |
579 | res = await orderContract.sendApprove(rndSigner.getSender(), signerIdx, msgVal, rndQueryId);
580 |
581 | expect(res.transactions).toHaveTransaction({
582 | on: orderContract.address,
583 | from: rndSigner.address,
584 | op: Op.order.approve,
585 | success: false,
586 | aborted: true,
587 | exitCode: Errors.order.unauthorized_sign
588 | });
589 |
590 | // testApprove(res.transactions, rndSigner.address, orderContract.address, Errors.order.unauthorized_sign);
591 | expect(await getContractData(orderContract.address)).toEqualCell(dataBefore);
592 |
593 | // Just to be extra sure let's pick totally invalid index
594 | res = await orderContract.sendApprove(rndSigner.getSender(), signers.length + 100, msgVal, rndQueryId);
595 | expect(res.transactions).toHaveTransaction({
596 | on: orderContract.address,
597 | from: rndSigner.address,
598 | op: Op.order.approve,
599 | success: false,
600 | aborted: true,
601 | exitCode: Errors.order.unauthorized_sign
602 | });
603 | expect(await getContractData(orderContract.address)).toEqualCell(dataBefore);
604 | });
605 | it('should reject approval if already approved', async () => {
606 | const signersNum = signers.length;
607 | const msgVal = toNano('0.1');
608 | const queryId = BigInt(getRandomInt(1000, 2000));
609 | // Pick random starting point
610 | let signerIdx = getRandomInt(0, signersNum - 1);
611 | for (let i = 0; i < 3; i++) {
612 | let signer = signers[signerIdx];
613 | let dataBefore = await orderContract.getOrderData();
614 | let res = await orderContract.sendApprove(signer.getSender(), signerIdx, msgVal, queryId);
615 | testApprove(res.transactions, signer.address, orderContract.address, 0);
616 | let dataAfter = await orderContract.getOrderData();
617 |
618 | expect(dataAfter.inited).toBe(true);
619 | expect(dataAfter.approvals_num).toEqual(dataBefore.approvals_num! + 1);
620 | expect(dataAfter._approvals).toBeGreaterThan(dataBefore._approvals!);
621 | expect(dataAfter.approvals[signerIdx]).toBe(true);
622 |
623 | dataBefore = dataAfter;
624 |
625 | // Repeat
626 | res = await orderContract.sendApprove(signer.getSender(), signerIdx, msgVal, queryId);
627 |
628 | testApprove(res.transactions, signer.address, orderContract.address, Errors.order.already_approved);
629 |
630 | dataAfter = await orderContract.getOrderData();
631 |
632 | // All should stay same
633 | expect(dataAfter.approvals_num).toEqual(dataBefore.approvals_num);
634 | expect(dataAfter._approvals).toEqual(dataBefore._approvals);
635 | // Make sure it doesn't reset
636 | expect(dataAfter.approvals[signerIdx]).toBe(true);
637 |
638 | // Increment, but respect array length
639 | signerIdx = ++signerIdx % signersNum;
640 | }
641 | });
642 |
643 | it('should reject execution when executed once', async () => {
644 | const msgVal = toNano('1');
645 | for (let i = 0; i < threshold; i++) {
646 | const res = await orderContract.sendApprove(signers[i].getSender(), i, msgVal);
647 | testApprove(res.transactions, signers[i].address, orderContract.address, 0);
648 | // Meh! TS made me do dat!
649 | if(i == threshold - 1) {
650 | expect(res.transactions).toHaveTransaction({
651 | from: orderContract.address,
652 | to: multisig.address,
653 | op: Op.multisig.execute
654 | });
655 | }
656 | }
657 |
658 | const dataAfter = await orderContract.getOrderData();
659 | expect(dataAfter.executed).toBe(true);
660 |
661 | const lateSigner = signers[threshold];
662 | expect(dataAfter.approvals[threshold]).toBe(false); // Make sure we're not failing due to occupied approval index
663 |
664 | const res = await orderContract.sendApprove(lateSigner.getSender(), threshold, msgVal);
665 |
666 | testApprove(res.transactions, lateSigner.address, orderContract.address, Errors.order.already_executed);
667 |
668 | // No execution message
669 | expect(res.transactions).not.toHaveTransaction({
670 | from: orderContract.address,
671 | to: multisig.address,
672 | op: Op.multisig.execute
673 | });
674 | });
675 |
676 | it('should handle 255 signers', async () => {
677 | const jumboSigners = await blockchain.createWallets(255);
678 | const jumboOrder = blockchain.openContract(Order.createFromConfig({
679 | multisig: multisig.address,
680 | orderSeqno: 1
681 | }, code));
682 |
683 | let res = await jumboOrder.sendDeploy(multisig.getSender(), toNano('1'), jumboSigners.map(s => s.address), blockchain.now! + 1000, mockOrder, jumboSigners.length);
684 |
685 | expect(res.transactions).toHaveTransaction({
686 | from: multisig.address,
687 | to: jumboOrder.address,
688 | deploy: true,
689 | success: true
690 | });
691 |
692 | // Now let's vote
693 |
694 | for (let i = 0; i < jumboSigners.length; i++) {
695 | res = await jumboOrder.sendApprove(jumboSigners[i].getSender(), i);
696 | testApprove(res.transactions, jumboSigners[i].address, jumboOrder.address, 0);
697 |
698 | const dataAfter = await jumboOrder.getOrderData();
699 | expect(dataAfter.approvals_num).toEqual(i + 1);
700 | expect(dataAfter.approvals[i]).toBe(true);
701 | }
702 |
703 | expect(res.transactions).toHaveTransaction({
704 | from: jumboOrder.address,
705 | to: multisig.address,
706 | op: Op.multisig.execute,
707 | });
708 | });
709 |
710 | });
711 |
--------------------------------------------------------------------------------
/tests/utils.ts:
--------------------------------------------------------------------------------
1 | import { randomAddress, compareTransaction, flattenTransaction, FlatTransactionComparable } from "@ton/test-utils";
2 | import {Address, Transaction, Cell, Dictionary, Message} from '@ton/core';
3 | import { Blockchain, BlockchainTransaction } from "@ton/sandbox";
4 | import { extractEvents } from "@ton/sandbox/dist/event/Event";
5 |
6 | export const differentAddress = (oldAddr:Address) => {
7 |
8 | let newAddr : Address;
9 |
10 | do {
11 | newAddr = randomAddress(oldAddr.workChain);
12 | } while(newAddr.equals(oldAddr));
13 |
14 | return newAddr;
15 | }
16 | export const getRandom = (min:number, max:number) => {
17 | return Math.random() * (max - min) + min;
18 | }
19 |
20 | export const getRandomInt = (min:number, max:number) => {
21 | return Math.round(getRandom(min, max));
22 | }
23 |
24 | export const findTransaction = (txs: T[], match: FlatTransactionComparable) => {
25 | return txs.find(x => compareTransaction(flattenTransaction(x), match));
26 | }
27 |
28 | export const getMsgPrices = (configRaw: Cell, workchain: 0 | -1 ) => {
29 |
30 | const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell());
31 |
32 | const prices = config.get(25 + workchain);
33 |
34 | if(prices === undefined) {
35 | throw Error("No prices defined in config");
36 | }
37 |
38 | const sc = prices.beginParse();
39 | let magic = sc.loadUint(8);
40 |
41 | if(magic != 0xea) {
42 | throw Error("Invalid message prices magic number!");
43 | }
44 | return {
45 | lumpPrice:sc.loadUintBig(64),
46 | bitPrice: sc.loadUintBig(64),
47 | cellPrice: sc.loadUintBig(64),
48 | ihrPriceFactor: sc.loadUintBig(32),
49 | firstFrac: sc.loadUintBig(16),
50 | nextFrac: sc.loadUintBig(16)
51 | };
52 | }
53 |
54 | export const storageCollected = (trans:Transaction) => {
55 | if(trans.description.type !== "generic")
56 | throw("Expected generic transaction");
57 | return trans.description.storagePhase ? trans.description.storagePhase.storageFeesCollected : 0n;
58 | };
59 | export const computedGeneric = (trans:Transaction) => {
60 | if(trans.description.type !== "generic")
61 | throw("Expected generic transaction");
62 | if(trans.description.computePhase.type !== "vm")
63 | throw("Compute phase expected")
64 | return trans.description.computePhase;
65 | };
66 |
67 | type MsgQueued = {
68 | msg: Message,
69 | parent?: BlockchainTransaction
70 | };
71 | export class Txiterator implements AsyncIterator {
72 | private msqQueue: MsgQueued[];
73 | private blockchain: Blockchain;
74 |
75 | constructor(bc:Blockchain, msg: Message) {
76 | this.msqQueue = [{msg}];
77 | this.blockchain = bc;
78 | }
79 |
80 | public async next(): Promise> {
81 | if(this.msqQueue.length == 0) {
82 | return {done: true, value: undefined};
83 | }
84 | const curMsg = this.msqQueue.shift()!;
85 | const inMsg = curMsg.msg;
86 | if(inMsg.info.type !== "internal")
87 | throw(Error("Internal only"));
88 | const smc = await this.blockchain.getContract(inMsg.info.dest);
89 | const res = smc.receiveMessage(inMsg, {now: this.blockchain.now});
90 | const bcRes = {
91 | ...res,
92 | events: extractEvents(res),
93 | parent: curMsg.parent,
94 | children: [],
95 | externals: []
96 | }
97 | for(let i = 0; i < res.outMessagesCount; i++) {
98 | const outMsg = res.outMessages.get(i)!;
99 | // Only add internal for now
100 | if(outMsg.info.type === "internal") {
101 | this.msqQueue.push({msg:outMsg, parent: bcRes})
102 | }
103 | }
104 | return {done: false, value: bcRes};
105 | }
106 | };
107 |
108 | export const executeTill = async (txs: AsyncIterable | AsyncIterator, match: FlatTransactionComparable) => {
109 | let executed: BlockchainTransaction[] = [];
110 | let txIterable = txs as AsyncIterable;
111 | let txIterator = txs as AsyncIterator;
112 | if(txIterable[Symbol.asyncIterator]) {
113 | for await (const tx of txIterable) {
114 | executed.push(tx);
115 | if(compareTransaction(flattenTransaction(tx), match)) {
116 | return executed;
117 | }
118 | }
119 | }
120 | else {
121 | let iterResult = await txIterator.next();
122 | while(!iterResult.done){
123 | executed.push(iterResult.value);
124 | if(compareTransaction(flattenTransaction(iterResult.value), match)) {
125 | return executed;
126 | }
127 | iterResult = await txIterator.next();
128 | }
129 | }
130 | // Will fail with common error message format
131 | expect(executed).toHaveTransaction(match);
132 | return executed;
133 | }
134 | export const executeFrom = async (txs: AsyncIterator) => {
135 | let executed: BlockchainTransaction[] = [];
136 | let iterResult = await txs.next();
137 | while(!iterResult.done){
138 | executed.push(iterResult.value);
139 | iterResult = await txs.next();
140 | }
141 | return executed;
142 | }
143 |
--------------------------------------------------------------------------------
/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/Constants.ts:
--------------------------------------------------------------------------------
1 | export abstract class Op {
2 | static readonly multisig = {
3 | new_order : 0xf718510f,
4 | execute: 0x75097f5d,
5 | execute_internal: 0xa32c59bf
6 | }
7 | static readonly order = {
8 | approve: 0xa762230f,
9 | expired: 0x6,
10 | approve_rejected : 0xafaf283e,
11 | approved: 0x82609bf6,
12 | init: 0x9c73fba2
13 | }
14 | static readonly actions = {
15 | send_message: 0xf1381e5b,
16 | update_multisig_params: 0x1d0cfbd3,
17 | }
18 | }
19 |
20 | export abstract class Errors {
21 | static readonly multisig = {
22 | unauthorized_new_order : 1007,
23 | invalid_new_order : 1008,
24 | not_enough_ton : 100,
25 | unauthorized_execute : 101,
26 | singers_outdated : 102,
27 | invalid_dictionary_sequence: 103,
28 | expired: 111
29 | }
30 | static readonly order = {
31 | unauthorized_init : 104,
32 | already_approved : 107,
33 | already_inited : 105,
34 | unauthorized_sign : 106,
35 | expired: 111,
36 | unknown_op: 0xffff,
37 | already_executed: 112
38 | }
39 | };
40 |
41 | export abstract class Params {
42 | static readonly bitsize = {
43 | op : 32,
44 | queryId : 64,
45 | orderSeqno : 256,
46 | signerIndex : 8,
47 | actionIndex : 8,
48 | time: 48
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/wrappers/Librarian.compile.ts:
--------------------------------------------------------------------------------
1 | import { CompilerConfig } from '@ton/blueprint';
2 |
3 | export const compile:CompilerConfig = {
4 | targets: ['contracts/helper/librarian.func']
5 | };
6 |
--------------------------------------------------------------------------------
/wrappers/Librarian.ts:
--------------------------------------------------------------------------------
1 | import { Address, toNano, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode, Message, storeMessage } from '@ton/core';
2 |
3 |
4 | export type LibrarianConfig = {
5 | code: Cell;
6 | };
7 |
8 | export function librarianConfigToCell(config: LibrarianConfig): Cell {
9 | return config.code;
10 | }
11 | export class Librarian implements Contract {
12 | constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) {}
13 |
14 | async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) {
15 | await provider.internal(via, {
16 | value,
17 | sendMode: SendMode.PAY_GAS_SEPARATELY
18 | });
19 | }
20 |
21 | static createFromAddress(address: Address) {
22 | return new Librarian(address);
23 | }
24 |
25 | static createFromConfig(config: LibrarianConfig, code: Cell, workchain = -1) {
26 | const data = librarianConfigToCell(config);
27 | const init = { code, data };
28 | return new Librarian(contractAddress(workchain, init), init);
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/wrappers/Multisig.compile.ts:
--------------------------------------------------------------------------------
1 | import { CompilerConfig } from '@ton/blueprint';
2 | import { compile as compileFunc } from '@ton/blueprint';
3 |
4 | export const compile: CompilerConfig = {
5 | lang: 'func',
6 | preCompileHook: async () => {
7 | await compileFunc('Order');
8 | },
9 | targets: ['contracts/multisig.func'],
10 | };
11 |
--------------------------------------------------------------------------------
/wrappers/Multisig.ts:
--------------------------------------------------------------------------------
1 | import { Address, beginCell, Cell, Dictionary, MessageRelaxed, storeMessageRelaxed, Contract, contractAddress, ContractProvider, Sender, SendMode, internal, toNano } from '@ton/core';
2 | import { Op, Params } from "./Constants";
3 |
4 | export type Module = {
5 | address: Address,
6 | module: Cell
7 | };
8 | export type MultisigConfig = {
9 | threshold: number;
10 | signers: Array;
11 | proposers: Array;
12 | allowArbitrarySeqno:boolean;
13 | };
14 |
15 | export type TransferRequest = { type: 'transfer', sendMode:SendMode, message:MessageRelaxed};
16 | export type UpdateRequest = {
17 | type: 'update',
18 | threshold: number,
19 | signers: Array,
20 | proposers: Array
21 | };
22 |
23 | export type Action = TransferRequest | UpdateRequest;
24 | export type Order = Array;
25 |
26 | function arrayToCell(arr: Array): Dictionary {
27 | let dict = Dictionary.empty(Dictionary.Keys.Uint(8), Dictionary.Values.Address());
28 | for (let i = 0; i < arr.length; i++) {
29 | dict.set(i, arr[i]);
30 | }
31 | return dict;
32 | }
33 |
34 | function moduleArrayToCell(arr: Array) {
35 | let dict = Dictionary.empty(Dictionary.Keys.Address(), Dictionary.Values.Cell());
36 | for (let module of arr) {
37 | dict.set(module.address, module.module);
38 | }
39 | return dict;
40 | }
41 |
42 | function cellToArray(addrDict: Cell | null) : Array {
43 | let resArr: Array = [];
44 | if(addrDict !== null) {
45 | const dict = Dictionary.loadDirect(Dictionary.Keys.Uint(8), Dictionary.Values.Address(), addrDict);
46 | resArr = dict.values();
47 | }
48 | return resArr;
49 | }
50 |
51 |
52 | export function multisigConfigToCell(config: MultisigConfig): Cell {
53 | return beginCell()
54 | .storeUint(0, Params.bitsize.orderSeqno)
55 | .storeUint(config.threshold, Params.bitsize.signerIndex)
56 | .storeRef(beginCell().storeDictDirect(arrayToCell(config.signers)))
57 | .storeUint(config.signers.length, Params.bitsize.signerIndex)
58 | .storeDict(arrayToCell(config.proposers))
59 | .storeBit(config.allowArbitrarySeqno)
60 | .endCell();
61 | }
62 |
63 | export class Multisig implements Contract {
64 | public orderSeqno: number;
65 |
66 | constructor(readonly address: Address,
67 | readonly init?: { code: Cell; data: Cell },
68 | readonly configuration?: MultisigConfig) {
69 | this.orderSeqno = 0;
70 | }
71 |
72 | static createFromAddress(address: Address) {
73 | let multisig = new Multisig(address);
74 | multisig.orderSeqno = 0;
75 | return multisig;
76 | }
77 |
78 | static createFromConfig(config: MultisigConfig, code: Cell, workchain = 0) {
79 | const data = multisigConfigToCell(config);
80 | const init = { code, data };
81 | return new Multisig(contractAddress(workchain, init), init, config);
82 | }
83 |
84 | async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) {
85 | await provider.internal(via, {
86 | value,
87 | sendMode: SendMode.PAY_GAS_SEPARATELY,
88 | body: beginCell().storeUint(0, Params.bitsize.op)
89 | .storeUint(0, Params.bitsize.queryId)
90 | .endCell(),
91 | });
92 | }
93 |
94 | static packTransferRequest(transfer: TransferRequest) {
95 | let message = beginCell().store(storeMessageRelaxed(transfer.message)).endCell();
96 | return beginCell().storeUint(Op.actions.send_message, Params.bitsize.op)
97 | .storeUint(transfer.sendMode, 8)
98 | .storeRef(message)
99 | .endCell();
100 |
101 | }
102 | static packUpdateRequest(update: UpdateRequest) {
103 | return beginCell().storeUint(Op.actions.update_multisig_params, Params.bitsize.op)
104 | .storeUint(update.threshold, Params.bitsize.signerIndex)
105 | .storeRef(beginCell().storeDictDirect(arrayToCell(update.signers)))
106 | .storeDict(arrayToCell(update.proposers))
107 | .endCell();
108 | }
109 |
110 | packLarge(actions: Array, address?: Address) {
111 | return Multisig.packLarge(actions, address ?? this.address);
112 | }
113 | static packLarge(actions: Array, address: Address) : Cell {
114 | let packChained = function (req: Cell) : TransferRequest {
115 | return {
116 | type: "transfer",
117 | sendMode: 1,
118 | message: internal({
119 | to: address,
120 | value: toNano('0.01'),
121 | body: beginCell().storeUint(Op.multisig.execute_internal, Params.bitsize.op)
122 | .storeUint(0, Params.bitsize.queryId)
123 | .storeRef(req)
124 | .endCell()
125 | })
126 | }
127 | };
128 | let tailChunk : Cell | null = null;
129 | let chunkCount = Math.ceil(actions.length / 254);
130 | let actionProcessed = 0;
131 | let lastSz = actions.length % 254;
132 | while(chunkCount--) {
133 | let chunkSize : number;
134 | if(lastSz > 0) {
135 | chunkSize = lastSz;
136 | lastSz = 0;
137 | }
138 | else {
139 | chunkSize = 254
140 | }
141 |
142 | // Processing chunks from tail to head to evade recursion
143 | const chunk = actions.slice(-(chunkSize + actionProcessed), actions.length - actionProcessed);
144 |
145 | if(tailChunk === null) {
146 | tailChunk = Multisig.packOrder(chunk);
147 | }
148 | else {
149 | // Every next chunk has to be chained with execute_internal
150 | tailChunk = Multisig.packOrder([...chunk, packChained(tailChunk)]);
151 | }
152 |
153 | actionProcessed += chunkSize;
154 | }
155 |
156 | if(tailChunk === null) {
157 | throw new Error("Something went wrong during large order pack");
158 | }
159 |
160 | return tailChunk;
161 | }
162 | static packOrder(actions: Array) {
163 | let order_dict = Dictionary.empty(Dictionary.Keys.Uint(8), Dictionary.Values.Cell());
164 | if(actions.length > 255) {
165 | throw new Error("For action chains above 255, use packLarge method");
166 | }
167 | else {
168 | // pack transfers to the order_body cell
169 | for (let i = 0; i < actions.length; i++) {
170 | const action = actions[i];
171 | const actionCell = action.type === "transfer" ? Multisig.packTransferRequest(action) : Multisig.packUpdateRequest(action);
172 | order_dict.set(i, actionCell);
173 | }
174 | return beginCell().storeDictDirect(order_dict).endCell();
175 | }
176 | }
177 |
178 | static newOrderMessage(actions: Order | Cell,
179 | expirationDate: number,
180 | isSigner: boolean,
181 | addrIdx: number,
182 | order_id: bigint = 115792089237316195423570985008687907853269984665640564039457584007913129639935n,
183 | query_id: number | bigint = 0) {
184 |
185 | const msgBody = beginCell().storeUint(Op.multisig.new_order, Params.bitsize.op)
186 | .storeUint(query_id, Params.bitsize.queryId)
187 | .storeUint(order_id, Params.bitsize.orderSeqno)
188 | .storeBit(isSigner)
189 | .storeUint(addrIdx, Params.bitsize.signerIndex)
190 | .storeUint(expirationDate, Params.bitsize.time)
191 |
192 | if(actions instanceof Cell) {
193 | return msgBody.storeRef(actions).endCell();
194 | }
195 |
196 | if(actions.length == 0) {
197 | throw new Error("Order list can't be empty!");
198 | }
199 | let order_cell = Multisig.packOrder(actions);
200 | return msgBody.storeRef(order_cell).endCell();
201 | }
202 | async sendNewOrder(provider: ContractProvider, via: Sender,
203 | actions: Order | Cell,
204 | expirationDate: number, value: bigint = toNano('1'), addrIdx?: number, isSigner?: boolean, seqno?: bigint) {
205 |
206 | if(seqno == undefined) {
207 | seqno = 115792089237316195423570985008687907853269984665640564039457584007913129639935n;
208 | }
209 | if(this.configuration === undefined) {
210 | throw new Error("Configuration is not set: use createFromConfig or loadConfiguration");
211 | }
212 | // check that via.address is in signers
213 | // We can only check in advance when address is known. Otherwise we have to trust isSigner flag
214 | if(via.address !== undefined) {
215 | const addrCmp = (x: Address) => x.equals(via.address!);
216 | addrIdx = this.configuration.signers.findIndex(addrCmp);
217 | if(addrIdx >= 0) {
218 | isSigner = true;
219 | } else {
220 | addrIdx = this.configuration.proposers.findIndex(addrCmp);
221 | if (addrIdx < 0) {
222 | throw new Error("Sender is not a signer or proposer");
223 | }
224 | isSigner = false;
225 | }
226 | }
227 | else if(isSigner === undefined || addrIdx == undefined) {
228 | throw new Error("If sender address is not known, addrIdx and isSigner parameres required");
229 | }
230 |
231 | let newActions : Cell | Order;
232 |
233 | if(actions instanceof Cell) {
234 | newActions = actions;
235 | }
236 | else if(actions.length > 255) {
237 | newActions = Multisig.packLarge(actions, this.address);
238 | }
239 | else {
240 | newActions = Multisig.packOrder(actions);
241 | }
242 | await provider.internal(via, {
243 | sendMode: SendMode.PAY_GAS_SEPARATELY,
244 | value,
245 | body: Multisig.newOrderMessage(newActions, expirationDate, isSigner, addrIdx, seqno)
246 | });
247 | //console.log(await provider.get("get_order_address", []));
248 | }
249 |
250 | async getOrderAddress(provider: ContractProvider, orderSeqno: bigint) {
251 | const { stack } = await provider.get("get_order_address", [{type: "int", value: orderSeqno},]);
252 | return stack.readAddress();
253 | }
254 |
255 | async getOrderEstimate(provider: ContractProvider, order: Order, expiration_date: bigint) {
256 | const orderCell = Multisig.packOrder(order);
257 | const { stack } = await provider.get('get_order_estimate', [{type: "cell", cell: orderCell}, {type: "int", value: expiration_date}]);
258 | return stack.readBigNumber();
259 | }
260 |
261 | async getMultisigData(provider: ContractProvider) {
262 | const { stack } = await provider.get("get_multisig_data", []);
263 | const nextOrderSeqno = stack.readBigNumber();
264 | const threshold = stack.readBigNumber();
265 | const signers = cellToArray(stack.readCellOpt());
266 | const proposers = cellToArray(stack.readCellOpt());
267 | return { nextOrderSeqno, threshold, signers, proposers};
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/wrappers/Order.compile.ts:
--------------------------------------------------------------------------------
1 | import { CompilerConfig } from '@ton/blueprint';
2 | import { readFile, writeFile, mkdir } from 'fs/promises';
3 | import path from 'path';
4 |
5 | export const compile: CompilerConfig = {
6 | lang: 'func',
7 | postCompileHook: async (code) => {
8 | const auto = path.join(__dirname, '..', 'contracts', 'auto');
9 | await mkdir(auto, { recursive: true });
10 | await writeFile(path.join(auto, 'order_code.func'), `
11 | ;; https://docs.ton.org/tvm.pdf, page 30
12 | ;; Library reference cell — Always has level 0, and contains 8+256 data bits, including its 8-bit type integer 2
13 | ;; and the representation hash Hash(c) of the library cell being referred to. When loaded, a library
14 | ;; reference cell may be transparently replaced by the cell it refers to, if found in the current library context.
15 |
16 | cell order_code() asm "spec PUSHREF";`);
17 | },
18 | targets: ['contracts/order.func'],
19 | };
20 |
--------------------------------------------------------------------------------
/wrappers/Order.ts:
--------------------------------------------------------------------------------
1 | import { Address, beginCell, Cell, Builder, BitString, Dictionary, Contract, contractAddress, ContractProvider, Sender, SendMode, toNano } from '@ton/core';
2 | import { Op, Params } from "./Constants";
3 |
4 | export type OrderConfig = {
5 | multisig: Address,
6 | orderSeqno: number
7 | };
8 |
9 | function arrayToCell(arr: Array): Dictionary {
10 | let dict = Dictionary.empty(Dictionary.Keys.Uint(8), Dictionary.Values.Address());
11 | for (let i = 0; i < arr.length; i++) {
12 | dict.set(i, arr[i]);
13 | }
14 | return dict;
15 | }
16 |
17 | function cellToArray(addrDict: Cell | null) : Array {
18 | let resArr: Array = [];
19 | if(addrDict !== null) {
20 | const dict = Dictionary.loadDirect(Dictionary.Keys.Uint(8), Dictionary.Values.Address(), addrDict);
21 | resArr = dict.values();
22 | }
23 | return resArr;
24 | }
25 |
26 | export function orderConfigToCell(config: OrderConfig): Cell {
27 | return beginCell()
28 | .storeAddress(config.multisig)
29 | .storeUint(config.orderSeqno, Params.bitsize.orderSeqno)
30 | .endCell();
31 | }
32 |
33 | export class Order implements Contract {
34 | constructor(readonly address: Address,
35 | readonly init?: { code: Cell, data: Cell },
36 | readonly configuration?: OrderConfig) {}
37 |
38 | static createFromAddress(address: Address) {
39 | return new Order(address);
40 | }
41 |
42 | static createFromConfig(config: OrderConfig, code: Cell, workchain = 0) {
43 | const data = orderConfigToCell(config);
44 | const init = { code, data };
45 |
46 | return new Order(contractAddress(workchain, init), init, config);
47 | }
48 |
49 | static initMessage (signers: Array,
50 | expiration_date: number,
51 | order: Cell,
52 | threshold: number = 1,
53 | approve_on_init: boolean = false,
54 | signer_idx: number = 0,
55 | query_id : number | bigint = 0) {
56 |
57 | const msgBody = beginCell()
58 | .storeUint(Op.order.init, Params.bitsize.op)
59 | .storeUint(query_id, Params.bitsize.queryId)
60 | .storeUint(threshold, Params.bitsize.signerIndex)
61 | .storeRef(beginCell().storeDictDirect(arrayToCell(signers)))
62 | .storeUint(expiration_date, Params.bitsize.time)
63 | .storeRef(order)
64 | .storeBit(approve_on_init);
65 |
66 | if(approve_on_init) {
67 | msgBody.storeUint(signer_idx, Params.bitsize.signerIndex);
68 | }
69 |
70 | return msgBody.endCell();
71 | }
72 | async sendDeploy(provider: ContractProvider,
73 | via: Sender,
74 | value: bigint,
75 | signers: Array,
76 | expiration_date: number,
77 | order: Cell,
78 | threshold: number = 1,
79 | approve_on_init: boolean = false,
80 | signer_idx: number = 0,
81 | query_id : number | bigint = 0) {
82 |
83 |
84 | await provider.internal(via, {
85 | value,
86 | sendMode: SendMode.PAY_GAS_SEPARATELY,
87 | body: Order.initMessage(signers, expiration_date, order, threshold, approve_on_init, signer_idx, query_id)
88 | });
89 | }
90 |
91 | async sendApprove(provider: ContractProvider, via: Sender, signer_idx: number, value: bigint = toNano('0.1'), query_id: number | bigint = 0) {
92 | await provider.internal(via, {
93 | value,
94 | sendMode: SendMode.PAY_GAS_SEPARATELY,
95 | body: beginCell()
96 | .storeUint(Op.order.approve, Params.bitsize.op)
97 | .storeUint(query_id, Params.bitsize.queryId)
98 | .storeUint(signer_idx, Params.bitsize.signerIndex)
99 | .endCell()
100 | });
101 | }
102 |
103 |
104 | async getOrderData(provider: ContractProvider) {
105 | /*
106 | (slice multisig, int order_seqno, int threshold,
107 | int sent_for_execution?, cell signers,
108 | int approvals, int approvals_num, int expiration_date,
109 | cell order)
110 | */
111 | const { stack } = await provider.get("get_order_data", []);
112 | const multisig = stack.readAddress();
113 | const order_seqno = stack.readBigNumber();
114 | const threshold = stack.readNumberOpt();
115 | const executed = stack.readBooleanOpt();
116 | const signers = cellToArray(stack.readCellOpt());
117 | const approvals = stack.readBigNumberOpt();
118 | const approvals_num = stack.readNumberOpt();
119 | const expiration_date = stack.readBigNumberOpt();
120 | const order = stack.readCellOpt();
121 | let approvalsArray: Array;
122 | if(approvals !== null) {
123 | approvalsArray = Array(256);
124 | for(let i = 0; i < 256; i++) {
125 | approvalsArray[i] = Boolean((1n << BigInt(i)) & approvals);
126 | }
127 | }
128 | else {
129 | approvalsArray = [];
130 | }
131 | return {
132 | inited: threshold !== null, multisig, order_seqno, threshold, executed, signers,
133 | approvals: approvalsArray, approvals_num: approvals_num, _approvals : approvals, expiration_date, order
134 | };
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
|