模式:燙手山芋 (Pattern: Hot Potato)
能力系統中的一種特殊情況——不具任何能力的結構——稱為燙手山芋(hot potato)。它無法 被儲存(無論是作為物件,或是作為 另一個結構中的欄位)、無法被 複製或丟棄。因此,一旦 建立,就必須由其模組妥善解構,否則交易會因未使用且不具 drop 的值而中止。
如果你熟悉支援回呼(callback)的程式語言,可以將燙手山芋視為必須呼叫回呼函式的 義務。若未呼叫它,交易將會中止。
此名稱源自兒童遊戲:玩家之間快速傳遞一顆球,而當音樂停止時,沒有任何玩家 想成為最後持有它的人,否則便會出局。這是此模式最貼切的說明——燙手山芋結構的 執行個體會在呼叫之間傳遞,且沒有任何模組可以保留它。
定義燙手山芋 (Defining a Hot Potato)
燙手山芋可以是任何不具能力的結構。例如,下列結構就是燙手山芋:
public struct Request {}
由於 Request 不具任何能力,且無法被儲存或忽略,模組必須提供一個 函式來解構它。例如:
/// Constructs a new `Request`
public fun new_request(): Request { Request {} }
/// Unpacks the `Request`. Due to the nature of the hot potato, this function
/// must be called to avoid aborting the transaction.
public fun confirm_request(request: Request) {
let Request {} = request;
}
使用範例 (Example Usage)
在下列範例中,Promise 燙手山芋用於確保從容器取出的借用值會被歸還 至其中。Promise 結構包含借用物件的 ID 與容器的 ID, 確保借用值未被替換為其他值,並且會被歸還至正確的容器。
/// Trying to return value to incorrect container.
const ENotCorrectContainer: u64 = 0;
/// Trying to return incorrect value.
const ENotCorrectValue: u64 = 1;
/// A generic container for any Object with `key + store`. The Option type
/// is used to allow taking and putting the value back.
public struct Container<T: key + store> has key {
id: UID,
value: Option<T>,
}
/// A Hot Potato struct that is used to ensure the borrowed value is returned.
public struct Promise {
/// The ID of the borrowed object. Ensures that there wasn't a value swap.
id: ID,
/// The ID of the container. Ensures that the borrowed value is returned to
/// the correct container.
container_id: ID,
}
/// A function that allows borrowing the value from the container.
public fun borrow_val<T: key + store>(container: &mut Container<T>): (T, Promise) {
let value = container.value.extract();
let id = object::id(&value);
(value, Promise { id, container_id: object::id(container) })
}
/// Put the taken item back into the container.
public fun return_val<T: key + store>(
container: &mut Container<T>, value: T, promise: Promise
) {
let Promise { id, container_id } = promise;
assert!(object::id(container) == container_id, ENotCorrectContainer);
assert!(object::id(&value) == id, ENotCorrectValue);
container.value.fill(value);
}
應用情境 (Applications)
以下列出燙手山芋模式的一些常見使用情境。
借用 (Borrowing)
如上述範例所示,燙手山芋非常適合用於借用,並能保證 借用值會歸還至正確的容器。雖然範例著重於儲存在 Option 內的值, 相同模式也可套用至任何其他儲存類型,例如 動態欄位。
閃電貸款 (Flash Loans)
燙手山芋模式的典型範例是閃電貸款——在同一筆交易中借入並償還的 貸款。借入的資金可用於執行某些操作,償還的資金則會歸還給放款人。 燙手山芋模式可確保借入資金會歸還給放款人。
此模式的使用範例可能如下:
// 從放款人借入資金;`potato` 使我們必須償還。
let (funds, potato) = lender.borrow(amount);
// 使用借入資金執行一些操作。
let asset = dex.trade(funds);
let proceeds = another_contract::do_something(asset);
// 償還貸款並保留利潤。
let payback = proceeds.split(amount, ctx);
lender.repay(payback, potato);
transfer::public_transfer(proceeds, ctx.sender());
未處理的燙手山芋也會影響交易其餘部分允許執行的操作:與其 糾纏的值在燙手山芋被消耗之前,無法傳遞給非 public 的 entry 函式。 精確規則——包括一個完整的閃電貸款範例——請參閱 入口函式。
可變路徑執行 (Variable-path Execution)
燙手山芋模式可用於在執行路徑中引入變化。例如,若有一個模組允許使用 某些「Bonus Points」或 USD 購買 Phone,即可使用燙手山芋將購買與付款 解耦。此方法非常類似某些商店的運作方式——你從架上拿取商品, 接著前往收銀台付款。
/// Trying to purchase `Phone` with incorrect price of `BonusPoints` or `USD`.
const ENotCorrectPrice: u64 = 0;
/// A `Phone`. Can be purchased in a store.
public struct Phone has key, store { id: UID }
/// A ticket that must be paid to purchase the `Phone`.
public struct Ticket { amount: u64 }
/// Return the `Phone` and the `Ticket` that must be paid to purchase it.
public fun purchase_phone(ctx: &mut TxContext): (Phone, Ticket) {
(
Phone { id: object::new(ctx) },
Ticket { amount: 100 }
)
}
/// The customer may pay for the `Phone` with `BonusPoints`.
public fun pay_in_bonus_points(ticket: Ticket, payment: Coin<BONUS>) {
let Ticket { amount } = ticket;
assert!(payment.value() == amount, ENotCorrectPrice);
abort // omitting the rest of the function
}
/// The customer may pay for the `Phone` with `USD`.
public fun pay_in_usd(ticket: Ticket, payment: Coin<USD>) {
let Ticket { amount } = ticket;
assert!(payment.value() == amount, ENotCorrectPrice);
abort // omitting the rest of the function
}
這種解耦技術可將購買邏輯與付款邏輯分離,使原始碼更模組化且更容易維護。 Ticket 可拆分為獨立模組,為付款提供基本介面;商店實作也可擴充以支援其他 商品,而無須變更付款邏輯。
組合式模式 (Compositional Patterns)
燙手山芋可用於以組合方式串連不同模組。其模組可以定義與燙手山芋互動的方式, 例如為其蓋上型別簽章,或從中擷取某些資訊。如此一來,燙手山芋便可在不同模組之間 傳遞,甚至可在同一筆交易中的不同套件之間傳遞。
在 Sui Framework 中的使用 (Usage in the Sui Framework)
此模式以多種形式用於 Sui Framework。以下是一些範例:
- sui::borrow - 使用燙手山芋確保借用值會歸還至 正確的容器。
- sui::transfer_policy - 定義 TransferRequest——一種燙手山芋, 唯有滿足所有條件時才能被消耗。
- sui::token - 在封閉迴圈代幣系統中,ActionRequest 攜帶 已執行操作的資訊,並以類似 TransferRequest 的方式收集核准。
- sui::package - 守護 套件升級流程的 UpgradeTicket 與 UpgradeReceipt 是燙手山芋:已授權的升級必須在同一筆交易中執行並提交。
總結 (Summary)
- 燙手山芋是不具能力的結構;其模組必須提供建立與銷毀它的方法。
- 燙手山芋用於確保交易結束前會採取某項操作,類似回呼。
- 燙手山芋最常見的使用情境為借用、閃電貸款、可變路徑執行與組合式模式。
下一節介紹的 Scratchpad 是另一種限於單一交易範圍內的 機制。兩者看似相似,但用途不同——請參閱該節結尾的比較。