I have a problem with transaction rollback when transferring Jetton tokens: for example one user wants to transfer Jetton tokens to another user whose owner is another smart contract. The transfer is done in three transactions:
Actually the problem may be the following: when the contract receives a tokenNotification message from the wallet contract and for some reason wants to roll back this transaction (for example, in the code that is responsible for processing the tokenNotification message, there are some checks that have not been passed), then he will be able to cancel only the transaction with the tokenNotification message (that is the third transaction), but the token transfer itself was in the second.
One of the proposed options was to send tokens back to the user, but the problem here is that transferring tokens back is another transaction that is initiated by my contract, which means my contract must pay a commission for the transfer. That is, the user can thus attack my contract, so that he will send many transactions that my contract will consider invalid and will try to send tokens back, spending his TON on the commission.
Here is code of my Jetton contract
@interface("org.ton.jetton.wallet")
contract JettonDefaultWallet {
const minTonsForStorage: Int = ton("0.019");
const gasConsumption: Int = ton("0.013");
balance: Int as coins = 0;
owner: Address;
master: Address;
init(owner: Address, master: Address){
self.balance = 0;
self.owner = owner;
self.master = master;
}
receive(msg: TokenTransfer){
// 0xf8a7ea5
let ctx: Context = context(); // Check sender
require(ctx.sender == self.owner, "Invalid sender");
let final: Int =
(((ctx.readForwardFee() * 2 + 2 * self.gasConsumption) + self.minTonsForStorage) + msg.forward_ton_amount); // Gas checks, forward_ton = 0.152
require(ctx.value > final, "Invalid value");
// Update balance
self.balance = (self.balance - msg.amount);
require(self.balance >= 0, "Invalid balance");
let init: StateInit = initOf JettonDefaultWallet(msg.sender, self.master);
let wallet_address: Address = contractAddress(init);
send(SendParameters{
to: wallet_address,
value: 0,
mode: SendRemainingValue,
bounce: true,
body: TokenTransferInternal{ // 0x178d4519
query_id: msg.query_id,
amount: msg.amount,
from: self.owner,
response_destination: msg.response_destination,
forward_ton_amount: msg.forward_ton_amount,
forward_payload: msg.forward_payload
}.toCell(),
code: init.code,
data: init.data
}
);
}
receive(msg: TokenTransferInternal){
// 0x178d4519
let ctx: Context = context();
if (ctx.sender != self.master) {
let sinit: StateInit = initOf JettonDefaultWallet(msg.from, self.master);
require(contractAddress(sinit) == ctx.sender, "Invalid sender!");
}
// Update balance
self.balance = (self.balance + msg.amount);
require(self.balance >= 0, "Invalid balance");
// Get value for gas
let msg_value: Int = self.msg_value(ctx.value);
let fwd_fee: Int = ctx.readForwardFee();
if (msg.forward_ton_amount > 0) {
msg_value = ((msg_value - msg.forward_ton_amount) - fwd_fee);
send(SendParameters{
to: self.owner,
value: msg.forward_ton_amount,
mode: SendPayGasSeparately,
bounce: false,
body: TokenNotification{ // 0x7362d09c -- Remind the new Owner
query_id: msg.query_id,
amount: msg.amount,
from: msg.from,
forward_payload: msg.forward_payload
}.toCell()
}
);
}
// 0xd53276db -- Cashback to the original Sender
if (msg.response_destination != null && msg_value > 0) {
send(SendParameters{
to: msg.response_destination!!,
value: msg_value,
bounce: false,
body: TokenExcesses{query_id: msg.query_id}.toCell(),
mode: SendPayGasSeparately
}
);
}
}
receive(msg: TokenBurn){
let ctx: Context = context();
require(ctx.sender == self.owner, "Invalid sender"); // Check sender
self.balance = (self.balance - msg.amount); // Update balance
require(self.balance >= 0, "Invalid balance");
let fwd_fee: Int = ctx.readForwardFee(); // Gas checks
require(ctx.value > ((fwd_fee + 2 * self.gasConsumption) + self.minTonsForStorage), "Invalid value - Burn");
// Burn tokens
send(SendParameters{
to: self.master,
value: 0,
mode: SendRemainingValue,
bounce: true,
body: TokenBurnNotification{
query_id: msg.query_id,
amount: msg.amount,
sender: self.owner,
response_destination: msg.response_destination
}.toCell()
}
);
}
fun msg_value(value: Int): Int {
let msg_value1: Int = value;
let ton_balance_before_msg: Int = (myBalance() - msg_value1);
let storage_fee: Int = (self.minTonsForStorage - min(ton_balance_before_msg, self.minTonsForStorage));
msg_value1 = (msg_value1 - (storage_fee + self.gasConsumption));
return msg_value1;
}
bounced(msg: bounced<TokenTransferInternal>){
self.balance = (self.balance + msg.amount);
}
bounced(msg: bounced<TokenBurnNotification>){
self.balance = (self.balance + msg.amount);
}
get fun get_wallet_data(): JettonWalletData {
return
JettonWalletData{
balance: self.balance,
owner: self.owner,
master: self.master,
code: initOf JettonDefaultWallet(self.owner, self.master).code
};
}
}
There are some misconceptions in what you have described:
when the contract [let's call it R] receives a tokenNotification message from the wallet contract and for some reason wants to roll back this transaction (for example, in the code that is responsible for processing the tokenNotification message, there are some checks that have not been passed), then he will be able to cancel only the transaction with the tokenNotification message (that is the third transaction), but the token transfer itself was in the second.
in your terms R won't be able to roll back even the third transaction. Let's make a diagram:
S -[tokenTransfer]-> SW -[transferInternal]-> RW -[tokenNotification]-> R
each message is denoted here with -[op_name]->
, contracts are in between. Transactions are done on each contract, i.e. a tx consists of:
Once a message is sent, the tx is complete, and you can't roll it back. If R
has received the tokenNotification
from RW
, than the tx on RW
is complete.
What you can do is to
R
can bounce tokenNotification
), and it depends on RW
implementation what RW
will do with the bounced message. If it supports rolling back the whole saga, it will revert some changes and send another bounce message to SW
etc, but most likely RW
will just ignore it (well, I'm almost sure that that's the case for Jettons as I've read some of their code);Note that even bouncing will require some commision (sending message requires gas), so any approach to reverting this chain requires some amount of TON. The only way to automate such reverting is to
tokenTransfer
message to allow the reverting process (ideally, in the case when reverting is not done, you have to also implement sending extra TON [not used for reverting] back from R
to S
).