Fake Jetton Contract
TON smart contracts use the transfer_notification
message sent by the receiver's Jetton wallet contract to specify and process a user request along with the transfer of a Jetton. Users add a forward_payload
to the Jetton transfer
message when transferring their Jettons, this forward_payload
is forwarded by the receiver's Jetton wallet contract to the receiver in the transfer_notification
message. The transfer_notification
message has the following TL-B schema:
transfer_notification#7362d09c query_id:uint64 amount:(VarUInteger 16)
sender:MsgAddress forward_payload:(Either Cell ^Cell)
= InternalMsgBody;
The amount
and sender
are added by the receiver's Jetton wallet contract as the amount of Jettons transferred and the sender of Jettons (owner of the Jetton wallet that sent of the internal_transfer
message). However, all the other values specified by the user in the forward_payload
are not parsed or validated by the Jetton wallet contract, they are added as it and sent in the transfer_notification
message. Therefore, the receiver of the transfer_notification
message must consider malicious values in the forward_payload
and validate them properly to prevent any contract state manipulation.
Example
The following simplified code highlights the lack of token_id validation in the transfer_notification
message. This contract tracks user deposits by updating the token0_balances
dictionary entry for the user's address. However, the transfer_notification
message handler does not verify that the sender_address
is equal to one of the token0
or token1
Jetton wallets owned by this contract. This allows users to manipulate their deposit values by sending the transfer_notification
message from a fake Jetton wallet contract.
#include "imports/stdlib.fc";
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
slice cs = in_msg_full.begin_parse();
int flags = cs~load_uint(4);
;; ignore all bounced messages
if (is_bounced?(flags)) {
return ();
}
slice sender_address = cs~load_msg_addr(); ;; incorrectly assumed to be Jetton wallet contract owned by this contract
(cell token0_balances, cell token1_balances) = load_data(); ;; balances dictionaries
(int op, int query_id) = in_msg_body~load_op_and_query_id();
if (op == op::transfer_notification) {
(int amount, slice from_address) = (in_msg_body~load_coins(), in_msg_body~load_msg_addr());
cell forward_payload_ref = in_msg_body~load_ref();
slice forward_payload = forward_payload_ref.begin_parse();
int is_token0? = forward_payload~load_int(1);
if (is_token0?) {
slice balance_before = token0_balances.dict_get?(267, from_address);
int balance = balance_before~load_coins();
balance = balance + amount;
slice balance_after = begin_cell().store_coinds(balance).end_cell().being_parse();
token0_balances~dict_set(267, from_address, balance_after);
} else {
slice balance_before = token1_balances.dict_get?(267, from_address);
int balance = balance_before~load_coins();
balance = balance + amount;
slice balance_after = begin_cell().store_coinds(balance).end_cell().being_parse();
token1_balances~dict_set(267, from_address, balance_after);
}
save_data();
return ();
}
}
Mitigations
- Store the address of Jetton wallet contract owned by the current contract at the time of contract initialization and use this stored value to verify the sender of the
transfer_notification
message. - Validate all the user provided values in the
forward_payload
instead of trusting users to send correct values.