我在转移 Jetton 代币时遇到交易回滚问题:例如,一个用户想要将 Jetton 代币转移给另一个用户,而该用户的所有者是另一个智能合约。转账分三笔完成:
实际上问题可能是这样的:当合约收到钱包合约发来的 tokenNotification 消息后,由于某种原因想要回滚这笔交易(例如,在负责处理 tokenNotification 消息的代码中,有一些检查尚未通过),那么他将只能取消带有 tokenNotification 消息的交易(即第三笔交易),但令牌传输本身在第二笔交易中。
建议的选项之一是将代币发送回给用户,但这里的问题是,将代币转回是我的合约发起的另一笔交易,这意味着我的合约必须为转账支付佣金。也就是说,用户可以攻击我的合约,这样他就会发送许多我的合约认为无效的交易,并尝试将代币发回,并花费他的 TON 来支付佣金。
这是我的 Jetton 合约代码
@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
};
}
}
您所描述的内容存在一些误解:
当合约[我们称之为R]从钱包合约接收到tokenNotification消息,并且出于某种原因想要回滚该交易时(例如,在负责处理tokenNotification消息的代码中,有一些检查尚未通过),那么他将只能取消带有 tokenNotification 消息的交易(即第三笔交易),但令牌传输本身在第二笔交易中。
按照你的说法,R 即使是第三笔交易也无法回滚。我们画个图吧:
S -[tokenTransfer]-> SW -[transferInternal]-> RW -[tokenNotification]-> R
每条消息都用
-[op_name]->
表示,合约位于两者之间。交易在每个合约上完成,即交易包括:
消息一旦发送,交易就完成了,并且无法回滚。如果
R
已收到来自 tokenNotification
的 RW
,则 RW
上的交易已完成。
你能做的是
R
可以退回tokenNotification
),这取决于RW
的实现,RW
将如何处理退回的消息。如果它支持回滚整个传奇,它将恢复一些更改并向 SW
等发送另一条退回消息,但很可能 RW
会忽略它(好吧,我几乎可以肯定 Jettons 就是这种情况,因为我读过他们的一些代码);请注意,即使弹跳也需要一些佣金(发送消息需要 Gas),因此任何恢复该链的方法都需要一定数量的 TON。自动执行此类恢复的唯一方法是
tokenTransfer
消息发送足够的 TON 以允许恢复过程(理想情况下,在恢复未完成的情况下,您还必须实现从 R
发送额外的 TON [不用于恢复] 到 S
).