Hot Potato 模式
Hot Potato(烫手山芋)模式是 Move 中一种独特而强大的设计模式。其核心是一个没有任何能力(abilities)的结构体——它不能被存储、不能被复制、不能被丢弃。就像一个真正的烫手山芋,一旦创建就必须被“消耗“掉,否则交易会失败。
这种模式可以在没有回调机制的情况下强制执行特定的工作流程,是 Move 类型系统最精妙的应用之一。
什么是 Hot Potato
在 Move 中,结构体可以拥有四种能力:copy、drop、store、key。一个没有任何能力的结构体具有以下特性:
| 操作 | 是否允许 | 原因 |
|---|---|---|
| 复制 | ❌ | 没有 copy |
| 丢弃 | ❌ | 没有 drop |
| 存储到对象中 | ❌ | 没有 store |
| 作为对象存在 | ❌ | 没有 key |
| 转移给其他地址 | ❌ | 没有 key |
唯一的处理方式是在同一个交易中通过解构(destructure)来消耗它。这意味着必须调用某个接受该类型并解构它的函数。
/// Hot Potato - 没有任何能力!
public struct Receipt {
amount: u64,
}
/// 创建 Hot Potato
public fun create_receipt(amount: u64): Receipt {
Receipt { amount }
}
/// 消耗 Hot Potato - 唯一的"出路"
public fun consume_receipt(receipt: Receipt): u64 {
let Receipt { amount } = receipt;
amount
}
为什么叫“烫手山芋“
想象你拿到一个滚烫的山芋:
- 不能拿着不动(不能 drop)——交易结束时如果还持有,交易失败
- 不能放进口袋(不能 store)——无法存储在任何对象中
- 不能递给别人(不能 transfer)——没有 key,不能作为独立对象转移
- 必须处理掉(必须解构)——唯一的解决方案
这就强制了调用者必须在同一个交易中完成整个工作流程。
闪电贷示例
闪电贷(Flash Loan)是 Hot Potato 模式最经典的应用场景。借款人必须在同一交易中借款并还款,否则交易会回滚:
module examples::flash_loan;
use sui::balance::{Self, Balance};
use sui::coin::{Self, Coin};
use sui::sui::SUI;
/// Hot Potato! 没有任何能力 - 必须被消耗
public struct FlashLoanReceipt {
amount: u64,
fee: u64,
}
public struct LendingPool has key {
id: UID,
balance: Balance<SUI>,
fee_percent: u64,
}
public fun create_pool(ctx: &mut TxContext) {
let pool = LendingPool {
id: object::new(ctx),
balance: balance::zero(),
fee_percent: 1,
};
transfer::share_object(pool);
}
public fun deposit(pool: &mut LendingPool, coin: Coin<SUI>) {
balance::join(&mut pool.balance, coin::into_balance(coin));
}
/// 借款 - 返回资金和一个 Hot Potato 收据
public fun borrow(
pool: &mut LendingPool,
amount: u64,
ctx: &mut TxContext,
): (Coin<SUI>, FlashLoanReceipt) {
let coins = coin::from_balance(
balance::split(&mut pool.balance, amount),
ctx,
);
let receipt = FlashLoanReceipt {
amount,
fee: amount * pool.fee_percent / 100,
};
(coins, receipt)
}
/// 还款 - 消耗 Hot Potato
const EInsufficientRepay: u64 = 0;
public fun repay(
pool: &mut LendingPool,
payment: Coin<SUI>,
receipt: FlashLoanReceipt,
) {
let FlashLoanReceipt { amount, fee } = receipt;
let repay_amount = amount + fee;
assert!(coin::value(&payment) >= repay_amount, EInsufficientRepay);
balance::join(&mut pool.balance, coin::into_balance(payment));
}
调用流程必须是:
borrow() → [使用资金做其他操作] → repay()
如果调用者只调用 borrow() 不调用 repay(),交易会失败,因为 FlashLoanReceipt 无法被丢弃。资金安全得到了类型系统的保证。
借用与归还模式
另一个常见场景是确保借出的资源一定会被归还:
module examples::lending;
use std::string::String;
public struct Item has key, store {
id: UID,
name: String,
}
/// Hot Potato - 借用凭证
public struct BorrowReceipt {
item_id: ID,
borrower: address,
}
public struct Vault has key {
id: UID,
items: vector<Item>,
}
/// 从保险柜借出物品,返回物品和凭证
public fun borrow_item(
vault: &mut Vault,
index: u64,
ctx: &TxContext,
): (Item, BorrowReceipt) {
let item = vector::remove(&mut vault.items, index);
let receipt = BorrowReceipt {
item_id: object::id(&item),
borrower: ctx.sender(),
};
(item, receipt)
}
const EItemMismatch: u64 = 0;
/// 归还物品,消耗凭证
public fun return_item(
vault: &mut Vault,
item: Item,
receipt: BorrowReceipt,
) {
let BorrowReceipt { item_id, borrower: _ } = receipt;
assert!(object::id(&item) == item_id, EItemMismatch);
vector::push_back(&mut vault.items, item);
}
多步骤工作流
Hot Potato 可以用来强制执行多步骤的工作流程,确保每一步都不会被跳过:
module examples::phone_shop;
use sui::coin::{Self, Coin};
use sui::sui::SUI;
/// 手机
public struct Phone has key, store {
id: UID,
model: std::string::String,
}
/// Hot Potato:排队号
public struct QueueTicket {
customer: address,
}
/// Hot Potato:验货凭证
public struct InspectionSlip {
customer: address,
phone_id: ID,
}
/// 第一步:排队取号
public fun take_queue_number(ctx: &TxContext): QueueTicket {
QueueTicket { customer: ctx.sender() }
}
/// 第二步:选购手机(消耗排队号,产生验货凭证)
public fun select_phone(
ticket: QueueTicket,
phone: &Phone,
): InspectionSlip {
let QueueTicket { customer } = ticket;
InspectionSlip {
customer,
phone_id: object::id(phone),
}
}
const EPhoneMismatch: u64 = 0;
/// 第三步:付款取货(消耗验货凭证)
public fun pay_and_collect(
slip: InspectionSlip,
phone: Phone,
mut payment: Coin<SUI>,
shop_owner: address,
ctx: &mut TxContext,
) {
let InspectionSlip { customer, phone_id } = slip;
assert!(object::id(&phone) == phone_id, EPhoneMismatch);
let price = coin::split(&mut payment, 1000, ctx);
transfer::public_transfer(price, shop_owner);
transfer::public_transfer(payment, customer);
transfer::public_transfer(phone, customer);
}
这个例子强制了购买流程的三个步骤必须按顺序执行:
take_queue_number()→ 得到QueueTicketselect_phone()→ 消耗QueueTicket,得到InspectionSlippay_and_collect()→ 消耗InspectionSlip
跳过任何步骤都会导致 Hot Potato 无法被消耗,交易失败。
可变路径执行
Hot Potato 还可以支持多种不同的消耗路径,实现灵活的工作流:
module examples::multi_path;
public struct Obligation {
value: u64,
}
public fun create_obligation(value: u64): Obligation {
Obligation { value }
}
/// 路径 A:全额偿还
public fun fulfill_full(obligation: Obligation) {
let Obligation { value: _ } = obligation;
}
/// 路径 B:部分偿还 + 新义务
const EInvalidPartial: u64 = 0;
public fun fulfill_partial(
obligation: Obligation,
partial_amount: u64,
): Obligation {
let Obligation { value } = obligation;
assert!(partial_amount < value, EInvalidPartial);
Obligation { value: value - partial_amount }
}
/// 路径 C:由管理员豁免
public fun waive(
_admin: &examples::capability::AdminCap,
obligation: Obligation,
) {
let Obligation { value: _ } = obligation;
}
设计要点
1. 确保有消耗路径
每个 Hot Potato 都必须至少有一个公开的消耗函数,否则调用者永远无法完成交易:
/// ❌ 错误:没有公开的消耗函数
public struct Trap { value: u64 }
public fun create_trap(): Trap {
Trap { value: 0 }
// 调用者拿到 Trap 后无法处理!
}
// 消耗函数只在模块内部,外部无法调用
fun consume_trap(trap: Trap) {
let Trap { value: _ } = trap;
}
2. 验证一致性
在消耗函数中验证 Hot Potato 携带的数据与实际操作一致:
public fun repay(receipt: Receipt, payment: Coin<SUI>) {
let Receipt { amount } = receipt;
// ✅ 验证还款金额
assert!(coin::value(&payment) >= amount, 0);
}
3. 携带必要信息
Hot Potato 可以携带字段来传递创建时的上下文信息到消耗时:
public struct ActionReceipt {
expected_result: u64,
deadline_epoch: u64,
initiator: address,
}
小结
Hot Potato 模式利用 Move 类型系统中“无能力结构体必须被解构“的规则,在没有回调机制的情况下实现了强制工作流程执行。它就像一个必须被传递和处理的“烫手山芋“,确保了借贷必须归还、流程必须完成、义务必须履行。这是 Move 语言独有的设计模式,在闪电贷、借用归还、多步骤流程等场景中有着不可替代的作用。