Skip to main content

Pattern:熱馬鈴薯 (Pattern: Hot Potato)

能力系統中的一個特殊情況——一個沒有任何能力的結構體——被稱為 熱馬鈴薯 (hot potato)。它不能被 儲存(不能作為物件,也不能作為 另一個結構體中的欄位),也不能被 複製丟棄。因此,一旦被 建構出來,它必須被其模組優雅地拆解,否則 交易會因為存在未使用且沒有 drop 能力的值而中止。

如果你熟悉支援 callback 的語言,可以把熱馬鈴薯想像成一個 必須呼叫回呼函式的義務。如果你不呼叫它,交易就會中止。

這個名稱來自兒童遊戲,遊戲中一顆球會在玩家之間快速傳遞,沒有人 想在音樂停止時還拿著球,否則就會出局。 這正是這個 pattern 的最佳寫照——熱馬鈴薯結構體的實例會在 呼叫之間傳遞,沒有任何模組可以把它留下來。

定義熱馬鈴薯 (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)

以下我們列出一些熱馬鈴薯 pattern 的常見使用案例。

借用 (Borrowing)

上方範例所示,熱馬鈴薯對於借用非常有效, 可以保證借出的值會歸還到正確的容器。雖然該範例聚焦於 儲存在 Option 中的值,但同樣的 pattern 也可以套用到任何其他儲存型別,例如 動態欄位

閃電貸 (Flash Loans)

熱馬鈴薯 pattern 的經典範例就是閃電貸——一種在同一筆交易中 借出並償還的貸款。借出的資金被用來執行某些操作, 而償還的資金則歸還給放貸方。熱馬鈴薯 pattern 確保借出的資金 會歸還給放貸方。

這個 pattern 的範例用法可能像這樣:

// 向放貸方借出資金;`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());

一個尚未結清的熱馬鈴薯也會影響交易其餘部分被允許執行的操作: 與其糾纏在一起的值在熱馬鈴薯被消耗之前,不能傳遞給非 publicentry 函式。詳細規則——連同一個完整的閃電貸範例——說明於 進入點函式

可變路徑執行 (Variable-path Execution)

熱馬鈴薯 pattern 可以用來在執行路徑中引入變化。舉例來說,如果 有一個模組允許使用「紅利點數」或美金來購買 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 可以被拆分成獨立的模組, 提供付款的基本介面,而商店的實作則可以擴充以支援其他 商品,而不需要更改付款邏輯。

組合式 Pattern (Compositional Patterns)

熱馬鈴薯可以用來以組合方式連結不同的模組。它的模組可以 定義與熱馬鈴薯互動的方式,例如,用一個型別簽章來標記它,或是從中 擷取某些資訊。透過這種方式,熱馬鈴薯可以在不同的模組之間傳遞, 甚至在同一筆交易中的不同套件之間傳遞。

在 Sui Framework 中的用法 (Usage in the Sui Framework)

這個 pattern 以各種形式被用於 Sui Framework 中。以下是一些範例:

  • sui::borrow——使用熱馬鈴薯來確保借出的值會歸還到 正確的容器。
  • sui::transfer_policy——定義了 TransferRequest——一個 只有在滿足所有條件時才能被消耗的熱馬鈴薯。
  • sui::token——在封閉迴圈代幣系統中,ActionRequest 攜帶 關於已執行操作的資訊,並以類似 TransferRequest 的方式收集核准。
  • sui::package——守護 套件升級 流程的 UpgradeTicketUpgradeReceipt 都是熱馬鈴薯: 一個經過授權的升級必須在同一筆交易中被執行並提交。

總結 (Summary)

  • 熱馬鈴薯是一個沒有能力的結構體;它的模組必須提供建立和銷毀它的方式。
  • 熱馬鈴薯被用來確保某個動作在交易結束前被執行,類似於 回呼函式。
  • 熱馬鈴薯最常見的使用案例是借用、閃電貸、可變路徑執行,以及 組合式 pattern。