Overview
The multisig contract on the TON blockchain is a smart contract that allows multiple signers to collectively manage assets and approve actions. Key features include:
- Threshold Approvals: Requires a minimum number of approvals from the signers before executing an order.
- Order System: Supports the creation, validation, and execution of orders, which could involve sending arbitrary internal messages to other contracts or updating the contract parameters (e.g., signers and threshold).
- Flexibility: Dynamic updates are allowed, meaning the set of signers and approval thresholds can be modified through approved orders.
- Strict Validation: Sequence numbers and timestamps are used to ensure orders are processed in the correct order and are valid for execution.
- Cost Management: The contract calculates and ensures there are enough funds to process order execution, covering gas fees and storage costs.
Differences with Multisig on Ethereum and Solana
Different from Ethereum and Solana, Ton operates in a multi-shard environment, meaning the state is distributed across various chains (shards), which introduces complexity in terms of state synchronization and message passing between shards. Multisig implementation on Ton follows thie shard pattern. There is a main multisig contract manages key information like signers, proposers, threshold, etc. And it can create order contract which records detailed order information, manages processes like approval and execution. Signers and proposers interacts with multisig contract to create order where order contract is deployed. And signers interact with order contract to approve and execute order. During order execution process, order contract sends a message including order details to multisig contract to execute its order finally.
Architecture
Multisig Contract
This multisig contract on the TON blockchain allows multiple signers to manage shared assets. Key functionalities include:
- Multisig approvals: Actions require a minimum number of signers (threshold) to approve before execution.
- Order management: It supports creating, validating, and executing orders, which could include sending messages or updating the contract's signers and parameters.
- Validation: Ensures correct sequence and order of operations, like checking signers, proposers, and expiration dates.
- Dynamic updates: Allows updating multisig settings (e.g., signers and thresholds) through orders.
Overall, it provides secure shared control over assets by requiring group consensus.
Data
In the
load_data
function, six key storage variables are extracted from the smart contract's data.1.
next_order_seqno
- Type: Integer (
int
)
- Purpose: This keeps track of the next order sequence number that will be used for creating a new order. The contract checks that new orders follow the expected sequence number, and increments this value after creating each new order.
2.
threshold
- Type: Integer (
int
)
- Purpose: The threshold is the minimum number of signatures required to approve an order. For the multisig wallet to execute an order, the number of signers' approvals must meet or exceed this threshold.
3.
signers
- Type: Cell (
cell
)
- Purpose: This is a dictionary (
udict
) of the signers for the multisig wallet. Each signer is associated with an index (an integer) and an address (a slice). The dictionary is crucial in validating whether an account has the authority to approve an order.
4.
signers_num
- Type: Integer (
int
)
- Purpose: This is the total number of signers stored in the
signers
dictionary. It represents how many valid signers are part of the multisig wallet, which is used in conjunction with thethreshold
to determine the required approvals for executing an order.
5.
proposers
- Type: Cell (
cell
)
- Purpose: This is another dictionary of proposers, who are users authorized to propose new orders. Like
signers
, it is a dictionary that stores information on proposers' indices and addresses. Signers and proposers both can create new order. But proposer can’t approve order.
6.
allow_arbitrary_order_seqno
- Type: Boolean (
int
)
- Purpose: This flag determines whether the multisig wallet allows arbitrary order sequence numbers. If set to
true
, the contract allows orders to be executed without strictly following thenext_order_seqno
. This might be used in cases where the wallet needs more flexibility in processing orders, bypassing strict sequence validation.
/// --- contracts/multisig.func --- (int, int, cell, int, cell, int) load_data() inline { slice ds = get_data().begin_parse(); var data = ( ds~load_order_seqno(), ;; next_order_seqno ds~load_index(), ;; threshold ds~load_nonempty_dict(), ;; signers ds~load_index(), ;; signers_num ds~load_dict(), ;; proposers ds~load_bool() ;; allow_arbitrary_order_seqno ); ds.end_parse(); return data; }
OP#New Order
The
new_order
instruction in this multisig wallet contract handles the creation of new orders proposed by authorized addresses (either signers or proposers).Steps:
- Loading
order_seqno
The sequence number (
order_seqno
) is extracted from the incoming message. This represents the identifier of the new order being proposed.- Validating
order_seqno
- If the
order_seqno
equalsMAX_ORDER_SEQNO
, the contract assignsnext_order_seqno
to it. This allows a default value ofMAX_ORDER_SEQNO
to represent the current next sequence number. - Otherwise, it checks whether the
order_seqno
provided by the message equals the currentnext_order_seqno
. If it doesn’t, the transaction fails with aninvalid_new_order
error. - After validation, the
next_order_seqno
is incremented for the next incoming order.
allow_arbitrary_order_seqno
: If arbitrary order sequence numbers are not allowed the contract strictly validates the order_seqno
.- Extracting Order Information
signer?
: A boolean that indicates whether the order is coming from a signer (true
) or a proposer (false
). If it comes from a signer, the signer will automatically approve the order.index
: The index in the signers or proposers dictionary corresponding to the sender of the order.expiration_date
: The timestamp until which the order is valid.order_body
: The core content of the order (stored as acell
), which includes actions to be executed by the multisig contract. The action can be sending internal message to other contracts, or modifying multisig setting like signers and threshold.
- Authorization Check
- It retrieves the
expected_address
from either thesigners
orproposers
dictionary depending on whethersigner?
istrue
(signers) orfalse
(proposers). - If the expected address isn’t found, the contract throws an
unauthorized_new_order
error. - It further checks that the
sender_address
from the message matches theexpected_address
. If they don’t match, it throws anunauthorized_new_order
error. - Lastly, it ensures that the order hasn’t expired by comparing
expiration_date
with the current time (now()
). If the order is expired, the contract throws anexpired
error.
The contract checks the authorization of the sender:
- Calculating Minimal Order Cost
- The contract calculates the minimal cost to process the order using the
calculate_order_processing_cost
function. - The contract then checks if the message sent contains enough value (
msg_value
) to cover this minimal cost. If not, it throws anot_enough_ton
error.
- Calculating Order Contract Address
- The contract calculates the state initialization data for the new order. This state initialization (
state_init
) includes the order’s sequence number and is necessary to generate the address for the new order. - It then calculates the new order contract address using this state initialization.
- Building Order Contract Initialization Message Body
- It stores:
- The operation code (
op::init
) andquery_id
. - The multisig wallet’s threshold for approvals.
- The signers dictionary.
- The expiration date for the order.
- The order body.
- Whether the order is from a signer (
signer?
). - If the order is from a signer, the signer’s index is also included.
The contract builds the initialization message body for the new order smart contract.
- Sending the Initialization Message
order_address
: The target address for the new order contract.state_init
: The state initialization data.init_body
: The body of the message containing the order information.BOUNCEABLE
: This flag indicates that the message is bounceable.SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE
andSEND_MODE_BOUNCE_ON_ACTION_FAIL
: These flags specify that the contract will carry all remaining message value and that it will bounce the message if the action fails.
Finally, the contract sends the message to initialize the new order’s smart contract:
/// --- contracts/multisig.func --- () recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) { /// ... if (op == op::new_order) { /// Loading order_seqno int order_seqno = in_msg_body~load_order_seqno(); /// Validating order_seqno if (~ allow_arbitrary_order_seqno) { if (order_seqno == MAX_ORDER_SEQNO) { order_seqno = next_order_seqno; } else { throw_unless(error::invalid_new_order, (order_seqno == next_order_seqno)); } next_order_seqno += 1; } /// Extracting Order Information int signer? = in_msg_body~load_bool(); int index = in_msg_body~load_index(); int expiration_date = in_msg_body~load_timestamp(); cell order_body = in_msg_body~load_ref(); in_msg_body.end_parse(); /// Authorization Check (slice expected_address, int found?) = (signer? ? signers : proposers).udict_get?(INDEX_SIZE, index); throw_unless(error::unauthorized_new_order, found?); throw_unless(error::unauthorized_new_order, equal_slices_bits(sender_address, expected_address)); throw_unless(error::expired, expiration_date >= now()); /// Calculating Minimal Order Cost int minimal_value = calculate_order_processing_cost(order_body, signers, expiration_date - now()); throw_unless(error::not_enough_ton, msg_value >= minimal_value); /// Calculating Order Contract Address cell state_init = calculate_order_state_init(my_address(), order_seqno); slice order_address = calculate_address_by_state_init(BASECHAIN, state_init); /// Building Order Contract Initialization Message Body builder init_body = begin_cell() .store_op_and_query_id(op::init, query_id) .store_index(threshold) .store_nonempty_dict(signers) .store_timestamp(expiration_date) .store_ref(order_body) .store_bool(signer?); if (signer?) { init_body = init_body.store_index(index); } /// Sending the Initialization Message send_message_with_state_init_and_body( order_address, 0, state_init, init_body, BOUNCEABLE, SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE | SEND_MODE_BOUNCE_ON_ACTION_FAIL ); } /// ... }
Order Processing Cost
calculate_order_processing_cost
calculates minimal gas fee required to create the order. Those fees are used later during order approval and execution phases.Fees includes:
- Gas cost on Multisig contract
Computation fee: Gas cost on Multisig contract to create order.
- Forward cost for Multisig->Order message
Forward fee: multisig contract sends message to deploys and initializes order contract.
- Gas cost on Order initialisation
Computation fee: multisig contract initializes order contract.
- Storage cost on Order
Storage fee: order contract storage
- Gas cost on Order finalization
Computation fee: order contract finalizes order.
- Forward cost for Order->Multisig message
Forward fee: order contract sends message to multisig contract to execute order.
- Gas cost on Multisig till accept_message
Computation fee: multisig wallet
op::execute
cost till accept_message
/// --- contracts/order_helpers.func --- int calculate_order_processing_cost(cell order_body, cell signers, int duration) inline { {- There are following costs: 1) Gas cost on Multisig contract 2) Forward cost for Multisig->Order message 3) Gas cost on Order initialisation 4) Storage cost on Order 5) Gas cost on Order finalization 6) Forward cost for Order->Multisig message 7) Gas cost on Multisig till accept_message -} ;; compute_data_size is unpredictable in gas, so we need to measure gas prior to it and after ;; and add difference to MULTISIG_INIT_ORDER_GAS int initial_gas = gas_consumed(); (int order_cells, int order_bits, _) = compute_data_size(order_body, 8192); ;; max cells in external message = 8192 (int signers_cells, int signers_bits, _) = compute_data_size(signers, 512); ;; max 255 signers in dict, this max cells in dict = 511 int size_counting_gas = gas_consumed() - initial_gas; 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); 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); int storage_fees = get_storage_fee(BASECHAIN, duration, ORDER_STATE_BIT_OVERHEAD + order_bits + signers_bits, ORDER_STATE_CELL_OVERHEAD + order_cells + signers_cells); return gas_fees + forward_fees + storage_fees; }
Order State Init
Multisig contract uses library cell to store contract code . This saves foward fee during order contract deployment.
/// --- contracts/order_helpers.func --- cell calculate_order_state_init(slice multisig_address, int order_seqno) inline { {- https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L144 _ split_depth:(Maybe (## 5)) special:(Maybe TickTock) code:(Maybe ^Cell) data:(Maybe ^Cell) library:(Maybe ^Cell) = StateInit; -} return begin_cell() .store_uint(0, 2) ;; 0b00 - No split_depth; No special .store_maybe_ref(order_code()) .store_maybe_ref(pack_order_init_data(multisig_address, order_seqno)) .store_uint(0, 1) ;; Empty libraries .end_cell(); }
In Ton, there are different cell type. For cell to store code, it can be ordinary cell or library cell. Library cell contains 8-bit tag of library
0x02
followed by 256-bit of referenced cell hash./// --- contracts/auto/order_code.func --- ;; https://docs.ton.org/tvm.pdf, page 30 ;; Library reference cell — Always has level 0, and contains 8+256 data bits, including its 8-bit type integer 2 ;; and the representation hash Hash(c) of the library cell being referred to. When loaded, a library ;; reference cell may be transparently replaced by the cell it refers to, if found in the current library context. cell order_code() asm "<b 2 8 u, 0x6305a8061c856c2ccf05dcb0df5815c71475870567cab5f049e340bcf59251f3 256 u, b>spec PUSHREF";
From TVM paper, ordinary cell tag is -1. But how can 8 bits represent value [-1,255]? From my test, ordinary code cell’s leading 8 bits are
11111111
.OP#Execute Order
The
op::execute
instruction handles the process of validating and executing an order. It checks several conditions to ensure that the execution is authorized.Steps:
- Loading Data:
order_seqno
: The sequence number of the order.expiration_date
: The timestamp indicating when the order will expire.approvals_num
: The number of signatures (approvals) on the order.signers_hash
: A hash representing the current signers' dictionary.order_body
: The content of the order itself, stored as a cell.
The function starts by loading key data from the
in_msg_body
, including:- Calculating the Order Address:
It uses the
calculate_order_state_init
and calculate_address_by_state_init
functions to calculate the expected order_address
. This involves using the order_seqno
to determine the initialization state of the order and its address in the blockchain.- Authorization Check:
- The function checks whether the
sender_address
of the message matches the calculatedorder_address
. This ensures that the message originated from the correct order contract. - It also verifies whether the current
signers_hash
matches the expectedsigners.cell_hash()
and ensures that the number of approvals (approvals_num
) is greater than or equal to thethreshold
. These checks confirm that the correct set of signers approved the order and that the necessary threshold of signatures is met. - This means that if the mutisig setting changes after an order has been created, like the threshold and signers, then old orders may not be valid to execute.
- Expiration Check:
It checks whether the current time (
now()
) is less than the expiration_date
. This ensures that the order has not expired and is still valid for execution.- Executing the Order:
If all the checks pass, the
execute_order
function is called with the order_body
to process the actions defined in the order. This function may handle actions like sending messages, updating multisig parameters, etc.- Saving the Updated State:
After the execution, the function saves the updated contract state, including the sequence number, threshold, signers, number of signers, proposers, and whether arbitrary order sequence numbers are allowed.
() recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) { /// ... elseif (op == op::execute) { ;; check that sender is order smart-contract and check that it has recent ;; signers dict /// Loading Data: int order_seqno = in_msg_body~load_order_seqno(); int expiration_date = in_msg_body~load_timestamp(); int approvals_num = in_msg_body~load_index(); int signers_hash = in_msg_body~load_hash(); cell order_body = in_msg_body~load_ref(); in_msg_body.end_parse(); /// Calculating the Order Address: cell state_init = calculate_order_state_init(my_address(), order_seqno); slice order_address = calculate_address_by_state_init(BASECHAIN, state_init); /// Authorization Check: throw_unless(error::unauthorized_execute, equal_slices_bits(sender_address, order_address)); throw_unless(error::singers_outdated, (signers_hash == signers.cell_hash()) & (approvals_num >= threshold)); /// Expiration Check: throw_unless(error::expired, expiration_date >= now()); /// Executing the Order: (threshold, signers, signers_num, proposers)~execute_order(order_body); } /// Saving the Updated State: save_data(next_order_seqno, threshold, signers, signers_num, proposers, allow_arbitrary_order_seqno); /// ... }
The
execute_order
function is responsible for executing actions from an "order" in multisig contract. It processes each action stored in a dictionary within the order_body
and can handle two specific actions: sending a message and updating multisig parameters.Steps:
- Accept Message:
The
accept_message()
function is called to update execution gaslimit to contract balance to ensure that the contract can perform actions in order.
- Iterating Through Actions:
The function uses
udict_get_next?
to iterate through actions stored in theorder_body
. Each action has an index, and the function checks for a "next" action. It processes actions until no more are found (found?
flag becomesfalse
).
- Action Processing:
Each action is processed based on its opcode (
action_op
). The function checks the opcode with the following logic: - Send Message (
actions::send_message
): - The contract loads a mode (as an 8-bit unsigned integer) and sends a raw message using the
send_raw_message()
function with a reference to the message body. - Update Multisig Parameters (
actions::update_multisig_params
): - The
threshold
is updated by loading a new index from the action slice. - The
signers
dictionary is updated with a new dictionary loaded from the action. - The function uses
validate_dictionary_sequence()
to verify that the signers dictionary is valid and sequentially ordered. - The
signers_num
is updated and checked to ensure there is at least one signer. - The
threshold
is checked to ensure it’s greater than zero and less than or equal to the number of signers. - The
proposers
dictionary is also updated and validated similarly.
- Return Updated Storage:
After processing the actions, the updated state (
threshold
,signers
,signers_num
,proposers
) is returned as a new tuple.
/// --- contracts/multisig.func --- ((int, cell, int, cell), ()) ~execute_order((int, cell, int, cell) storage, cell order_body) impure inline { /// Accept Message: accept_message(); (int threshold, cell signers, int signers_num, cell proposers) = storage; /// Iterating Through Actions: int action_index = -1; do { (action_index, slice action, int found?) = order_body.udict_get_next?(ACTION_INDEX_SIZE, action_index); if (found?) { action = action.preload_ref().begin_parse(); /// Action Processing: int action_op = action~load_op(); /// Send Message if (action_op == actions::send_message) { int mode = action~load_uint(8); send_raw_message(action~load_ref(), mode); action.end_parse(); /// Update Multisig Parameters } elseif (action_op == actions::update_multisig_params) { threshold = action~load_index(); signers = action~load_nonempty_dict(); signers_num = validate_dictionary_sequence(signers); throw_unless(error::invalid_signers, signers_num >= 1); throw_unless(error::invalid_threshold, threshold > 0); throw_unless(error::invalid_threshold, threshold <= signers_num); proposers = action~load_dict(); validate_dictionary_sequence(proposers); action.end_parse(); } } } until (~ found?); /// Return Updated Storage: return ((threshold, signers, signers_num, proposers), ()); }
validate_dictionary_sequence
validates the sequential ordering of entries in a dictionary stored in a cell
./// --- contracts/multisig.func --- int validate_dictionary_sequence(cell dict) impure inline { int index = -1; int expected_index = 0; do { (index, slice value, int found?) = dict.udict_get_next?(INDEX_SIZE, index); if (found?) { throw_unless(error::invalid_dictionary_sequence, index == expected_index); expected_index += 1; } } until (~ found?); return expected_index; }
OP#Execute Internal
The
op::execute_internal
instruction plays a role in a chain of actions where the execution of one order triggers the execution of another. It only checks the sender is itself. /// --- contracts/multisig.func --- () recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) { /// ... elseif (op == op::execute_internal) { ;; we always trust ourselves, this feature is used to make chains of executions ;; where last action of previous execution triggers new one. throw_unless(error::unauthorized_execute, equal_slices_bits(sender_address, my_address())); cell order_body = in_msg_body~load_ref(); in_msg_body.end_parse(); (threshold, signers, signers_num, proposers)~execute_order(order_body); } /// Saving the Updated State: save_data(next_order_seqno, threshold, signers, signers_num, proposers, allow_arbitrary_order_seqno); /// ... }
Order Contract
Data
multisig_address
: A slice representing the address of the multisig contract. This address is the entity that manages the order.
order_seqno
: An integer representing the sequence number of the order. When executing order, it needs to pass this to multisig contract which will calculates order contract address to verify.
threshold
: The number of approvals required for an action to be executed. If it'snull
, it means the multisig is not yet initialized.
sent_for_execution?
: A boolean flag indicating whether the order has already been sent for execution. It ensures the order isn’t executed multiple times.
signers
: A dictionary (cell) holding the set of authorized signers, typically represented as a non-empty dictionary (usually with index or public key mappings).
approvals_mask
: A bitmask tracking which signers have approved the current order. Each bit represents whether a signer at a given index has approved the action.
approvals_num
: The number of signers who have approved the current action so far. It must equal or exceedthreshold
to execute the order.
expiration_date
: A timestamp representing when the order expires. The order must be approved and executed before this date.
order
: A reference cell that holds the details of the current order.
/// --- contracts/order.func --- () load_data() impure inline { slice ds = get_data().begin_parse(); multisig_address = ds~load_msg_addr(); order_seqno = ds~load_order_seqno(); if (ds.slice_bits() == 0) { ;; not initialized yet threshold = null(); sent_for_execution? = null(); signers = null(); approvals_mask = null(); approvals_num = null(); expiration_date = null(); order = null(); } else { threshold = ds~load_index(); sent_for_execution? = ds~load_bool(); signers = ds~load_nonempty_dict(); approvals_mask = ds~load_uint(MASK_SIZE); approvals_num = ds~load_index(); expiration_date = ds~load_timestamp(); order = ds~load_ref(); ds.end_parse(); } }
OP#Init
The
op::init
instruction is used to initialize order contract. It can be triggered by sending an init
message to the contract with certain parameters. The message is expected to come from the multisig_address
, and if the contract has not been initialized yet, the message will carry all the necessary data to set up a multisig process.Steps:
- Validation of Sender:
The contract checks whether the
sender_address
is the same as the multisig_address
. If not, it throws an unauthorized_init
error. Only the authorized multisig address can initialize the order.Case 1: First-Time Initialization
If the contract has not been initialized yet (
threshold
is null
), the message sets the initial parameters for a multisig order. This will not happen, because in the multisig wallet, it carries necessary information(code and data) to deploy and initialize the contract. Once the contract has been deployed, it has all necessary data.- Set Threshold:
Loads and sets the approval
threshold
, which is the number of approvals required to execute the multisig order.- Set Execution Flag:
Sets the flag indicating the order has not been executed yet.
- Load Signers:
Loads the list of signers from the message body. The signers are stored in a dictionary, where each entry maps a signer’s public key or address to an index.
- Set Approvals:
Initializes the approval mask and count to 0, meaning no approvals have been collected yet.
- Set Expiration Date:
Loads and sets the order's expiration date. The contract ensures the expiration date is in the future. If the date is in the past, it throws an
expired
error and bounces back any tokens sent with the message.- Load Order:
Loads the
order
data, which contains the action or instructions that will be executed once enough approvals are gathered.- Optional Initial Approval:
If the
approve_on_init
flag is set to true
, the message also contains an initial approval from one of the signers. The signer's index is loaded, and the contract calls add_approval(signer_index)
to register the approval. After that, it tries to execute the order (try_execute(query_id)
), if all approvals are met.- Save State:
After parsing the message, the new state is saved, ensuring all the initialized data is stored in the contract.
Case 2: Re-initialization
If the contract has already been initialized (i.e.,
threshold
is not null
), this op::init
instruction handles subsequent initialization attempts.- Ensure Matching Data:
The contract ensures the message's data matches the already initialized values. This includes the
threshold
, signers
, expiration_date
, and the order
. If any mismatch is detected, the contract throws an already_inited
error. This ensures only valid follow-up initialization messages are accepted.- Approve on Init:
The
approve_on_init
flag must be set to true
in subsequent init
messages. This indicates that the signer is providing their approval as part of the initialization.- Load and Validate Signer Index:
The message must contain the signer's index. The contract looks up the signer's address in the
signers
dictionary and checks that the signer is authorized (found?
must be true). If the signer is unauthorized, an unauthorized_sign
error is thrown.- Approve the Signer:
If everything is valid, the contract records the signer's approval for the order by calling
approve(signer_index)
.() recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) { /// ... if (op == op::init) { throw_unless(error::unauthorized_init, equal_slices_bits(sender_address, multisig_address)); if (null?(threshold)) { ;; Let's init /// Set Threshold: threshold = in_msg_body~load_index(); /// Set Execution Flag: sent_for_execution? = false; /// Load Signers: signers = in_msg_body~load_nonempty_dict(); /// Set Approvals: approvals_mask = 0; approvals_num = 0; /// Set Expiration Date: expiration_date = in_msg_body~load_timestamp(); throw_unless(error::expired, expiration_date >= now()); ;; in case of error TONs will bounce back to multisig /// Load Order: order = in_msg_body~load_ref(); /// Optional Initial Approval: int approve_on_init? = in_msg_body~load_bool(); if (approve_on_init?) { int signer_index = in_msg_body~load_index(); add_approval(signer_index); try_execute(query_id); } in_msg_body.end_parse(); /// Save State: save_data(); return (); } else { ;; order is inited second time, if it is inited by another oracle ;; we count it as approval /// Ensure Matching Data: throw_unless(error::already_inited, in_msg_body~load_index() == threshold); throw_unless(error::already_inited, in_msg_body~load_nonempty_dict().cell_hash() == signers.cell_hash()); throw_unless(error::already_inited,in_msg_body~load_timestamp() == expiration_date); throw_unless(error::already_inited, in_msg_body~load_ref().cell_hash() == order.cell_hash()); /// Approve on Init: int approve_on_init? = in_msg_body~load_bool(); throw_unless(error::already_inited, approve_on_init?); /// Load and Validate Signer Index: int signer_index = in_msg_body~load_index(); in_msg_body.end_parse(); (slice signer_address, int found?) = signers.udict_get?(INDEX_SIZE, signer_index); throw_unless(error::unauthorized_sign, found?); /// Approve the Signer: approve(signer_index, signer_address, query_id); return (); } } /// ... }
OP#Approve
op::approve
handles order approval by signers.It decodes message body to get the signer index, searches signer address in signer dict, and matches it with sender address.
After validation, it executes function
approve
to approve the order./// --- contracts/order.func --- () recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) { /// ... if (op == op::approve) { int signer_index = in_msg_body~load_index(); in_msg_body.end_parse(); (slice signer_address, int found?) = signers.udict_get?(INDEX_SIZE, signer_index); throw_unless(error::unauthorized_sign, found?); throw_unless(error::unauthorized_sign, equal_slices_bits(sender_address, signer_address)); approve(signer_index, sender_address, query_id); return (); } /// ... }
Approve
approve
handles the approval of a transaction by a specific signer, checks if the transaction can be executed, and provides feedback to the signer.Steps:
- Execution Flow (try-catch Block):
The function uses a
try-catch
block to handle any errors that may occur during the approval process, sending appropriate messages in case of success or failure.- Already Executed Check:
It begins by checking if the transaction has already been executed using the
sent_for_execution?
flag. If this flag is true, the contract throws an already_executed
error, indicating that no further approvals can be processed for this transaction.- Add Approval:
The
add_approval
function is called to mark the approval by this specific signer. It updates a bitmask (approvals_mask
) to reflect that this signer has approved the transaction and increments the approval count (approvals_num
).- Send Approval Response:
If the approval is valid, the contract sends a message to the signer's
response_address
indicating that the approval was accepted.- Attempt to Execute the Order:
After the approval, the contract attempts to execute the order if the number of approvals equals or exceeds the required threshold:
- Save Data:
Once the approval has been processed, the contract updates and saves its internal state.
- Error Handling (Catch Block)
If any part of the process fails (e.g., unauthorized approval, already approved, or an invalid state), the contract sends a rejection message to the signer's
response_address.
/// --- contracts/order.func --- () approve(int signer_index, slice response_address, int query_id) impure inline_ref { try { throw_if(error::already_executed, sent_for_execution?); add_approval(signer_index); send_message_with_only_body( response_address, 0, begin_cell().store_op_and_query_id(op::approve_accepted, query_id), NON_BOUNCEABLE, SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE | SEND_MODE_BOUNCE_ON_ACTION_FAIL ); try_execute(query_id); save_data(); } catch (_, exit_code) { send_message_with_only_body( response_address, 0, begin_cell() .store_op_and_query_id(op::approve_rejected, query_id) .store_uint(exit_code, 32), NON_BOUNCEABLE, SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE | SEND_MODE_BOUNCE_ON_ACTION_FAIL ); } }
add_approval
is responsible for recording an approval from a specific signer, and it works by manipulating bit masks and approval counts.Steps:
- Create a Bit Mask (
mask
):
The
mask
is calculated by shifting 1
left by the signer_index
. This means that a unique bit in the mask corresponds to each signer. For example, if signer_index
is 2, the mask will be 1 << 2
, resulting in the bit pattern 000...0100
. This bit pattern is used to keep track of approvals by individual signers.- Check for Duplicate Approvals:
throw_if(error::already_approved, approvals_mask & mask);
checks if the current signer has already approved the order by checking if the corresponding bit in approvals_mask
is set. If the bit is set, the signer has already approved, and an error (error::already_approved
) is thrown to prevent multiple approvals from the same signer.- Increase the Number of Approvals:
If the signer has not yet approved, the approval count (
approvals_num
) is incremented by 1. This is how the system tracks the total number of signers who have approved the transaction.- Update the Approval Mask:
approvals_mask |= mask;
sets the corresponding bit in approvals_mask
to mark this signer as having approved. The bitwise OR operation (|=
) adds this signer's approval to the cumulative mask./// --- contracts/order.func --- () add_approval(int signer_index) impure inline { int mask = 1 << signer_index; throw_if(error::already_approved, approvals_mask & mask); approvals_num += 1; approvals_mask |= mask; }
The
try_execute
function is responsible for checking whether the number of approvals has met or exceeded the required threshold and, if so, initiating the execution of an order.Steps:
- Approval Threshold Check:
if (approvals_num == threshold)
ensures that the number of approvals (approvals_num
) matches the required number of approvals (threshold
). Only when this condition is met does the function proceed to execute the order.- This ensures that the order is executed only after a sufficient number of signers have approved it.
- Message Sending:
- The function constructs and sends a message to the multisig contract. The message signals that the order should be executed.
- The message body is created using
begin_cell()
and stores several critical pieces of data: op::execute
andquery_id
: Specifies the operation (execution) and the ID of the current query.order_seqno
: The sequence number of the order, which will be used to calcualte order contract by multisig contract to validate.expiration_date
: The expiration date of the order.approvals_num
: The number of approvals.signers.cell_hash()
: A hash representing the signers who approved the order, for the order to be successfully executed, it requires order contract’s signeres match it in the multisig contract.order
: A reference to the order itself.
- Message Configuration:
- The message is marked as
NON_BOUNCEABLE
, meaning it won't return any remaining balance if the message fails to execute. - The send mode combines:
SEND_MODE_CARRY_ALL_BALANCE
: Carries all remaining balance from the contract in the transaction.SEND_MODE_BOUNCE_ON_ACTION_FAIL
: The message will "bounce" or fail if it cannot be processed.
- Execution Status Update:
sent_for_execution? = true;
marks the order as having been sent for execution, preventing further approvals from triggering redundant execution attempts.
/// --- contracts/order.func --- () try_execute(int query_id) impure inline_ref { if (approvals_num == threshold) { send_message_with_only_body( multisig_address, 0, begin_cell() .store_op_and_query_id(op::execute, query_id) .store_order_seqno(order_seqno) .store_timestamp(expiration_date) .store_index(approvals_num) .store_hash(signers.cell_hash()) .store_ref(order), NON_BOUNCEABLE, SEND_MODE_CARRY_ALL_BALANCE | SEND_MODE_BOUNCE_ON_ACTION_FAIL ); sent_for_execution? = true; } }
OP#Approve With Message
Signer can also use the op 0 instruction to approve order.
The difference between op#0 and op#approve is that it doesn’t require a signer index, but an approval message. And it checks whether the sender is one of signers iteratively which can be more expensive.
/// --- contracts/order.func --- () recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) { /// ... if (op == 0) { ;; message with text comment slice text_comment = get_text_comment(in_msg_body); throw_unless(error::unknown_op, equal_slices_bits(text_comment, "approve")); (int signer_index, int found_signer?) = find_signer_by_address(sender_address); throw_unless(error::unauthorized_sign, found_signer?); approve(signer_index, sender_address, cur_lt()); return (); } /// ... }
get_text_comment
uses snake encoding to encode string./// --- contracts/order.func --- slice get_text_comment(slice in_msg_body) impure inline { if (in_msg_body.slice_refs() == 0) { return in_msg_body; } ;;combine comment into one slice builder combined_string = begin_cell(); int need_exit = false; do { ;; store all bits from current cell ;; it's ok to overflow here, it means that comment is incorrect combined_string = combined_string.store_slice(in_msg_body.preload_bits(in_msg_body.slice_bits())); ;;and go to the next if (in_msg_body.slice_refs()) { in_msg_body = in_msg_body.preload_ref().begin_parse(); } else { need_exit = true; } } until (need_exit); return combined_string.end_cell().begin_parse(); }
find_signer_by_address
tries to find corresponding address in signer dict iteratively./// --- contracts/order.func --- (int, int) find_signer_by_address(slice signer_address) impure inline { int found_signer? = false; int signer_index = -1; do { (signer_index, slice value, int next_found?) = signers.udict_get_next?(INDEX_SIZE, signer_index); if (next_found?) { if (equal_slices_bits(signer_address, value)) { found_signer? = true; next_found? = false; ;; fast way to exit loop } } } until (~ next_found?); return (signer_index, found_signer?); }
Appendix
Library Creation
librarian.func
helps create library cell.The core of this contract isset_lib_code(lib_to_publish, 2);
- it accepts as input ordinary cell that needs to be published and flag=2 (means that everybody can use it). Note, that contract that publish cell pays for it's storage and storage in masterchain 1000x higher than in basechain. So library cell usage is only efficient for contracts used by thousands users. https://docs.ton.org/develop/data-formats/library-cells#publish-ordinary-cell-in-masterchain-library-context
/// --- contracts/helper/librarian.func --- #include "../imports/stdlib.fc"; #include "../messages.func"; const int DEFAULT_DURATION = 3600 * 24 * 365 * 10; ;; 10 years, can top-up in any time const int ONE_TON = 1000000000; ;; https://docs.ton.org/tvm.pdf, page 138, SETLIBCODE () set_lib_code(cell code, int mode) impure asm "SETLIBCODE"; cell empty_cell() asm "<b b> PUSHREF"; () recv_internal(int msg_value, cell in_msg_full, slice in_msg_body) impure { slice in_msg_full_slice = in_msg_full.begin_parse(); int msg_flags = in_msg_full_slice~load_msg_flags(); slice sender_address = in_msg_full_slice~load_msg_addr(); cell lib_to_publish = get_data(); int initial_gas = gas_consumed(); (int order_cells, int order_bits, _) = compute_data_size(lib_to_publish, 1000); ;; according network config, max cells in library = 1000 int size_counting_gas = gas_consumed() - initial_gas; int to_reserve = get_simple_compute_fee(MASTERCHAIN, size_counting_gas) + get_storage_fee(MASTERCHAIN, DEFAULT_DURATION, order_bits, order_cells); raw_reserve(to_reserve, RESERVE_BOUNCE_ON_ACTION_FAIL); send_message_with_only_body(sender_address, 0, begin_cell(), NON_BOUNCEABLE, SEND_MODE_CARRY_ALL_BALANCE); ;; https://docs.ton.org/tvm.pdf, page 138, SETLIBCODE 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); ;; brick contract set_code(empty_cell()); set_data(empty_cell()); }
import {toNano} from '@ton/core'; import {compile, NetworkProvider} from '@ton/blueprint'; import {Librarian} from "../wrappers/Librarian"; export async function run(provider: NetworkProvider) { const order_code_raw = await compile('Order'); // deploy lib const librarian_code = await compile('Librarian'); const librarian = provider.open(Librarian.createFromConfig({code: order_code_raw}, librarian_code)); await librarian.sendDeploy(provider.sender(), toNano("10")); }