Introduction
In smart contracts, we often need to allow certain actions only for specific actors.
A simple example is a wallet contract: we must authorize message senders to ensure only valid users can send funds.
So what options do we have?
In this article, I’d like to show three common ways to authorize messages in TON smart contracts.
1. Signature Verification
This mechanism is used for external messages.
Below is an example in FunC that checks a message signature.
It comes from the standard wallet-v3 contract:
https://github.com/ton-blockchain/ton/blob/d97fc197f07bb0070eeb3e6fcb8137a240ea5365/crypto/smartcont/wallet3-code.fc#L7
() recv_external(slice in_msg) impure {
var signature = in_msg~load_bits(512);
;; some code
var (stored_seqno, stored_subwallet, public_key) =
(ds~load_uint(32), ds~load_uint(32), ds~load_uint(256));
;;
throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key));
accept_message();
;; other code
}
As we can see, the contract verifies the message’s signature using the public key stored in the contract’s data.
Why this cannot be used for internal messages
There are two main reasons:
Smart contracts must be deterministic.
Most cryptographic algorithms rely on randomness, which cannot be generated deterministically inside a smart contract.Contract storage is public.
Therefore, private keys cannot be stored securely inside the contract.
Because of this, signature-based authorization only works for external messages.
2. Address Storage
This is the most widely used authorization method for internal messages.
We simply compare the sender’s address with the address stored in the contract’s storage.
Example from the standard NFT Collection contract:
https://github.com/ton-blockchain/nft-contract/blob/0b493104cd547d0fd52b7e6fd3c046fc365fe3d4/nft/nft-collection-editable.fc#L70
() recv_internal(cell in_msg_full, slice in_msg_body) impure {
if (in_msg_body.slice_empty?()) {
;; ignore empty messages
return ();
}
slice cs = in_msg_full.begin_parse();
int flags = cs~load_uint(4);
if (flags & 1) {
;; ignore all bounced messages
return ();
}
slice sender_address = cs~load_msg_addr();
int op = in_msg_body~load_uint(32);
int query_id = in_msg_body~load_uint(64);
var (owner_address, next_item_index, content, nft_item_code, royalty_params) = load_data();
;; some code
throw_unless(401, equal_slices(sender_address, owner_address));
;; some code
}
The contract stores the owner address, and then throws if the sender does not match it.
This method allows storing multiple authorized addresses for different roles.
However, it is not ideal when we need to authorize many senders, such as in Jetton contracts.
3. Address Calculation
When a contract must authorize an arbitrary number of senders, we need a different approach.
A great example is the Jetton Wallet contract.
Whenever jettons are transferred, the receiver must verify whether the sender wallet truly belongs to the same Jetton Master.
Example from the standard Jetton Wallet:
https://github.com/ton-blockchain/jetton-contract/blob/d55f228edb0eb477cb4845d67e0dacc6489c6b57/contracts/jetton-wallet.fc#L83
() receive_jettons(slice in_msg_body, slice sender_address, int my_ton_balance, int msg_value)
impure inline_ref {
(int status, int balance, slice owner_address, slice jetton_master_address) = load_data();
int query_id = in_msg_body~load_query_id();
int jetton_amount = in_msg_body~load_coins();
slice from_address = in_msg_body~load_msg_addr();
slice response_address = in_msg_body~load_msg_addr();
throw_unless(error::not_valid_wallet,
equal_slices_bits(jetton_master_address, sender_address)
|
equal_slices_bits(
calculate_user_jetton_wallet_address(
from_address,
jetton_master_address,
my_code()
),
sender_address
)
);
;; other code
}
Here, the contract calls
calculate_user_jetton_wallet_address(from_address, jetton_master_address, my_code())
and checks the result against the actual sender.
Note the use of my_code() — an instruction that returns the current contract code via the c7 register:
https://docs.ton.org/tvm/registers#c7-—-environment-information-and-global-variables
Important notes
- You can also store another contract’s code in your storage to calculate addresses for external contracts.
- Sometimes the sender must pass initialization parameters used during deployment. You must validate these parameters carefully.
- If both contracts use the same known parameter (e.g., admin address), it is safer to read it from your own storage, not from the message.
Conclusion
I hope this article is helpful for newcomers to TON smart-contract development.
Choosing the right authorization strategy is an important part of designing a contract’s architecture.
Happy coding! 🚀
Top comments (0)