Jetton(TON)代币转账交易撤销问题及合约代码咨询
Jetton代币转账回滚问题与解决方案
问题核心
向所有者为智能合约的账户转账时,流程分三步:
- 发送方钱包发起
tokenTransfer消息 - 接收方钱包执行
transferInternal完成代币入账 - 向接收方所有者合约发送
tokenNotification做校验
当第三步校验失败时,仅能撤销通知操作,但代币已在第二步完成转移。此前采用合约主动转回代币的方案会产生手续费,恶意用户可通过大量无效交易消耗我方TON手续费。
解决方案:两阶段验证+ bounce机制
调整转账流程,延迟代币入账,利用TON的消息bounce特性让用户承担回滚成本,避免合约资金损失。
1. 核心修改思路
- 暂存转账信息,验证前不修改余额
- 给所有者合约发送带bounce的
tokenNotification,验证失败时消息自动回退 - 回滚操作使用用户初始支付的TON覆盖手续费,合约不承担成本
2. 修改后的合约代码
@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; // 暂存待验证的转账信息 pending_transfers: Map<Int, PendingTransfer> = {}; struct PendingTransfer { amount: Int; from: Address; response_destination: Address?; forward_payload: Cell; } init(owner: Address, master: Address){ self.balance = 0; self.owner = owner; self.master = master; } receive(msg: TokenTransfer){ // 0xf8a7ea5 let ctx: Context = context(); require(ctx.sender == self.owner, "Invalid sender"); // 增加手续费预留,覆盖可能的回滚操作成本 let final: Int = (((ctx.readForwardFee() * 3 + 3 * self.gasConsumption) + self.minTonsForStorage) + msg.forward_ton_amount); require(ctx.value > final, "Invalid value"); 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!"); } // 暂存转账信息,不立即增加余额 let pending_id: Int = msg.query_id; self.pending_transfers.put(pending_id, PendingTransfer{ amount: msg.amount, from: msg.from, response_destination: msg.response_destination, forward_payload: msg.forward_payload }); // 发送带bounce的TokenNotification,验证失败时消息会回退 send(SendParameters{ to: self.owner, value: msg.forward_ton_amount, mode: SendPayGasSeparately, bounce: true, body: TokenNotification{ // 0x7362d09c query_id: msg.query_id, amount: msg.amount, from: msg.from, forward_payload: msg.forward_payload }.toCell() } ); } // 处理验证失败的bounce消息,执行回滚 bounced(msg: bounced<TokenNotification>){ let pending_id: Int = msg.query_id; let transfer: PendingTransfer = self.pending_transfers.get(pending_id)!!; self.pending_transfers.delete(pending_id); // 将代币转回发送方钱包 let init: StateInit = initOf JettonDefaultWallet(transfer.from, self.master); let sender_wallet: Address = contractAddress(init); send(SendParameters{ to: sender_wallet, value: 0, mode: SendRemainingValue, bounce: true, body: TokenTransferInternal{ query_id: msg.query_id, amount: transfer.amount, from: self.owner, response_destination: transfer.response_destination, forward_ton_amount: 0, forward_payload: null }.toCell(), code: init.code, data: init.data }); } // 接收所有者合约的验证确认,完成最终入账 receive(msg: TokenNotificationConfirm){ let pending_id: Int = msg.query_id; let transfer: PendingTransfer = self.pending_transfers.get(pending_id)!!; self.pending_transfers.delete(pending_id); self.balance = self.balance + transfer.amount; require(self.balance >= 0, "Invalid balance"); // 返还剩余TON给用户 let ctx: Context = context(); let msg_value: Int = self.msg_value(ctx.value); if (transfer.response_destination != null && msg_value > 0) { send(SendParameters{ to: transfer.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"); self.balance = (self.balance - msg.amount); require(self.balance >= 0, "Invalid balance"); let fwd_fee: Int = ctx.readForwardFee(); require(ctx.value > ((fwd_fee + 2 * self.gasConsumption) + self.minTonsForStorage), "Invalid value - Burn"); 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 }; } }
3. 配套要求
- 所有者合约需实现
TokenNotification处理逻辑:验证通过时发送自定义的TokenNotificationConfirm消息;验证失败时不接收消息,让消息自动bounce回接收方钱包。 - 可给
pending_transfers添加过期清理逻辑,避免存储冗余。
内容的提问来源于stack exchange,提问作者Denys
相关产品推荐
相关产品推荐

