Order in essence is sequential list of one or more actions executed by multisig wallet contract.
multisig_address
parent multisig address.order_seqno
sequential number of the order contract.threshold
number of signatures required to start order execution.sent_for_execution?
flag indication whether order has been executed alreadysigners
Dictionary containing contract addresses allowed to sign order executionapprovals_mask
Bit field wheretrue
bit at n-th position indicates approval granted from n-th signer.approvals_num
Total number of granted approvals.expiration_date
Once current time exceeds this timestamp, order can't be executed anymore.order
Cell containing action descriptors.
When executing message order, multisig will send arbitrary message described by action.
When executing update order, multisig will update it's parameters according to action.
Updatable parameters are:
signers
threshold
Order life cycle consists of following steps:
- Order initialization.
- Order approval(sign).
- Order execution.
- 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.
- 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).
- Order can't be executed after it's expiration date.
- Once approval is granted by signer, it can't be revoked.
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.
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.
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)
Make sure you have enough balance on your multisig.
Initialization message is sent from multisig
wallet, creating new order contract.
Only multisig_address
and order_seqno
are part of InitState structure (defining future contract address).
Rest of the order state parameters are passed in a message body.
approve_on_init
parameter set to true
indicates that initializer wants to sign for order during the initialization.
wallet->multisig->new order
Order approval may be granted either by initialization or approve message.
signer_index
field indicates index in signers
dictionary to check sender address against.
Approval by init will only be accepted from multisig address, where approve
message will be accepted if sender address match the address
at signers[signer_index]
.
multisig->order
Once number of approvals reaches threshold
, the
execute message
is sent back to multisig contract among with the whole order contract balance.
Multisig contract performs checks
on the input order data and accepts it
for execution if all checks passed successfully
order->multisig
Order contract balance should be enough for:
- Storage till
expiration_date
. - Execution of the multisig contract till order is accepted for execution
During order initialization we must make sure that message value would be enough to cover:
- Order initialization transaction chain.
- Order storage
- Execution of approve logic in case
approve_on_init
istrue
. - Order execution transaction chain in case
approve_on_init
istrue
andthreshold = 1
Fees are calculated in order_helper.fc#53
Constants are used as a base values, however size of signers cell and order body size is dynamic. Therefore dynamically calculated sizes and gas consumption is added to those base values.
int initial_gas = gas_consumed();
(int order_cells, int order_bits, _) = compute_data_size(order_body, 8192);
(int signers_cells, int signers_bits, _) = compute_data_size(signers, 512);
int size_counting_gas = gas_consumed() - initial_gas;
To get these gas consumption constants, we use two test cases.
- With small signers size:tests/FeeComputation.spec.ts#L199 referred as "small signers"
- With maximum signers size:tests/FeeComputation.spec.ts#L205 referred as "large test"
contracts/order_helpers.func#L63
Forward fees are required to cover message sending.
For all of the overheads, the small signers
test is used.
Related constants:
INIT_ORDER_BITS_OVERHEAD
and INIT_ORDER_CELL_OVERHEAD
Represent total bits and cells used by order init message reduced by
bits and cells occupied by order body.
tests/FeeComputation.spec.ts#L123
/*
tx0 : external -> treasury
tx1: treasury -> multisig
tx2: multisig -> order
*/
let orderBody = (await order.getOrderData()).order;
let orderBodyStats = collectCellStats(orderBody!, []);
let multisigToOrderMessage = res.transactions[2].inMessage!;
let multisigToOrderMessageStats = computeMessageForwardFees(curMsgPrices, multisigToOrderMessage).stats;
let initOrderStateOverhead = multisigToOrderMessageStats.sub(orderBodyStats);
EXECUTE_ORDER_BIT_OVERHEAD
and EXECUTE_ORDER_CELL_OVERHEAD
follow the same logic, but for execute message order->multisig
.
tests/FeeComputation.spec.ts#L149
/*
tx0 : external -> treasury
tx1: treasury -> order
tx2: order -> treasury (approve)
tx3: order -> multisig
tx4+: multisig -> destination
*/
let orderToMultiownerMessage = secondApproval.transactions[3].inMessage!;
let orderToMultiownerMessageStats = computeMessageForwardFees(curMsgPrices, orderToMultiownerMessage).stats;
let orderToMultiownerMessageOverhead = orderToMultiownerMessageStats.sub(orderBodyStats);
Adds up with dynamic order body and signer sizes for calculation of the related forward fees.
int forward_fees = get_forward_fee(BASECHAIN,
INIT_ORDER_BIT_OVERHEAD + order_bits + signers_bits,
INIT_ORDER_CELL_OVERHEAD + order_cells + signers_cells) +
get_forward_fee(BASECHAIN,
EXECUTE_ORDER_BIT_OVERHEAD + order_bits,
EXECUTE_ORDER_CELL_OVERHEAD + order_cells);
Gas units are bound to the TVM instructions being executed where gas fees may change with the network configuration. Thus we rely on gas units.
contracts/order_helpers.func#L75
MULTISIG_INIT_ORDER_GAS
represents total gas units
required for execution of op::new_order
on multisig side.
large signers
test is used, and then size_counting_gas
is deducted.
In order to get size_counting_gas
, one would have to dump the value manually.
int size_counting_gas = gas_consumed() - initial_gas;
size_counting_gas~dump();
record the value and restore the source file.
tests/FeeComputation.spec.ts#L106
/*
tx0 : external -> treasury
tx1: treasury -> multisig
tx2: multisig -> order
*/
let MULTISIG_INIT_ORDER_GAS = computedGeneric(res.transactions[1]).gasUsed;
ORDER_INIT_GAS
is amount of gas
consumed by order contract while processing op::init
.
small signers
test is used due to it is not impacted by signers or order size.
tests/FeeComputation.spec.ts#L108
let ORDER_INIT_GAS = computedGeneric(res.transactions[2]).gasUsed;
ORDER_EXECUTE_GAS
is amount of gas consumed by order contract
once execution threshold is reached.
large signers
test is used, because dictionary lookup cost depends on dictionary size.
contracts/order.func#L109
Calculations:
tests/FeeComputation.spec.ts#L157
let ORDER_EXECUTE_GAS = computedGeneric(secondApproval.transactions[1]).gasUsed;
MULTISIG_EXECUTE_GAS
is amount of gas consumed
by multisig prior to accepting order execution.
For simplicity we use cost of execution of a 1 message order.
small signers
test is used, due to it is not impacted by signers size.
tests/FeeComputation.spec.ts#L159
let MULTISIG_EXECUTE_GAS = actions.length > 1 ? 7310n : computedGeneric(secondApproval.transactions[3]).gasUsed;
While for a fact it's only required to cover gas till contracts/multisig.func#L23
These constants summ up with size_counting
dynamic value and gas fee is calculated for each
one of those separately.
contracts/order_helpers.func#L70
int gas_fees = get_compute_fee(BASECHAIN,MULTISIG_INIT_ORDER_GAS + size_counting_gas) +
get_compute_fee(BASECHAIN, ORDER_INIT_GAS) +
get_compute_fee(BASECHAIN, ORDER_EXECUTE_GAS) +
get_compute_fee(BASECHAIN, MULTISIG_EXECUTE_GAS);
ORDER_STATE_BIT_OVERHEAD
and ORDER_STATE_CELL_OVERHEAD
is how many bits and
cells is occupied by order contract state without order body.
small signers
test is used, because signers
and order
overhead is added
dynamically
tests/FeeComputation.spec.ts#L125
let initOrderStateOverhead = multisigToOrderMessageStats.sub(orderBodyStats);
contracts/order_helpers.func#L83
int storage_fees = get_storage_fee(BASECHAIN, duration,
ORDER_STATE_BIT_OVERHEAD + order_bits + signers_bits,
ORDER_STATE_CELL_OVERHEAD + order_cells + signers_cells);