From 81753be9ad04add46e7a14a3145d0e4c03972a52 Mon Sep 17 00:00:00 2001 From: Damir Shamanaev Date: Wed, 24 Jul 2024 17:57:52 +0200 Subject: [PATCH] [book] Hot Potato (#87) * adds hot-potato page * use mainnet version in packages * Update book/src/programmability/hot-potato-pattern.md Co-authored-by: Ashok Menon * Update book/src/programmability/hot-potato-pattern.md Co-authored-by: Ashok Menon * Update book/src/programmability/hot-potato-pattern.md Co-authored-by: Ashok Menon * Update book/src/programmability/hot-potato-pattern.md Co-authored-by: Ashok Menon * examples compile * usage in the framework section --------- Co-authored-by: Ashok Menon --- book/src/SUMMARY.md | 2 +- .../src/programmability/hot-potato-pattern.md | 121 ++++++++++++++++++ book/src/programmability/hot-potato.md | 1 - packages/hello_world/Move.toml | 3 +- packages/reference/Move.toml | 2 +- packages/samples/Move.toml | 2 +- .../programmability/hot-potato-pattern.move | 98 ++++++++++++++ packages/todo_list/Move.toml | 2 +- 8 files changed, 224 insertions(+), 7 deletions(-) create mode 100644 book/src/programmability/hot-potato-pattern.md delete mode 100644 book/src/programmability/hot-potato.md create mode 100644 packages/samples/sources/programmability/hot-potato-pattern.move diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 6d0817df..bd6ed635 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -109,7 +109,7 @@ - [Events](./programmability/events.md) - [Balance & Coin]() - [Sui Framework](./programmability/sui-framework.md) - - [Pattern: Hot Potato]() + - [Pattern: Hot Potato](./programmability/hot-potato-pattern.md) - [Pattern: Request]() - [Pattern: Object Capability]() - [Package Upgrades]() diff --git a/book/src/programmability/hot-potato-pattern.md b/book/src/programmability/hot-potato-pattern.md new file mode 100644 index 00000000..82022d42 --- /dev/null +++ b/book/src/programmability/hot-potato-pattern.md @@ -0,0 +1,121 @@ +# Pattern: Hot Potato + +A case in the abilities system - a struct without any abilities - is called _hot potato_. It cannot +be stored (not as [an object](./../storage/key-ability.md) nor as +[a field in another struct](./../storage/store-ability.md)), it cannot be +[copied](./../move-basics/copy-ability.md) or [discarded](./../move-basics/drop-ability.md). Hence, +once constructed, it must be gracefully [unpacked by its module](./../move-basics/struct.md), or the +transaction will abort due to unused value without drop. + +> If you're familiar with languages that support _callbacks_, you can think of a hot potato as an +> obligation to call a callback function. If you don't call it, the transaction will abort. + +The name comes from the children's game where a ball is passed quickly between players, and none of +the players want to be the last one holding it when the music stops, or they are out of the game. +This is the best illustration of the pattern - the instance of a hot-potato struct is passed between +calls, and none of the modules can keep it. + +## Defining a Hot Potato + +A hot potato can be any struct with no abilities. For example, the following struct is a hot potato: + +```move +{{#include ../../../packages/samples/sources/programmability/hot-potato-pattern.move:definition}} +``` + +Because the `Request` has no abilities and cannot be stored or ignored, the module must provide a +function to unpack it. For example: + +```move +{{#include ../../../packages/samples/sources/programmability/hot-potato-pattern.move:new_request}} +``` + +## Example Usage + +In the following example, the `Promise` hot potato is used to ensure that the borrowed value, when +taken from the container, is returned back to it. The `Promise` struct contains the ID of the +borrowed object, and the ID of the container, ensuring that the borrowed value was not swapped for +another and is returned to the correct container. + +```move +{{#include ../../../packages/samples/sources/programmability/hot-potato-pattern.move:container_borrow}} +``` + +## Applications + +Below we list some of the common use cases for the hot potato pattern. + +### Borrowing + +As shown in the [example above](#example-usage), the hot potato is very effective for borrowing with +a guarantee that the borrowed value is returned to the correct container. While the example focuses +on a value stored inside an `Option`, the same pattern can be applied to any other storage type, say +a [dynamic field](./dynamic-fields.md). + +### Flash Loans + +Canonical example of the hot potato pattern is flash loans. A flash loan is a loan that is borrowed +and repaid in the same transaction. The borrowed funds are used to perform some operations, and the +repaid funds are returned to the lender. The hot potato pattern ensures that the borrowed funds are +returned to the lender. + +An example usage of this pattern may look like this: + +```move +// Borrow the funds from the lender. +let (asset_a, potato) = lender.borrow(amount); + +// Perform some operations with the borrowed funds. +let asset_b = dex.trade(loan); +let proceeds = another_contract::do_something(asset_b); + +// Keep the commission and return the rest to the lender. +let pay_back = proceeds.split(amount, ctx); +lender.repay(pay_back, potato); +transfer::public_transfer(proceeds, ctx.sender()); +``` + +### Variable-path execution + +The hot potato pattern can be used to introduce variation in the execution path. For example, if +there is a module which allows purchasing a `Phone` for some "Bonus Points" or for USD, the hot +potato can be used to decouple the purchase from the payment. The approach is very similar to how +some shops work - you take the item from the shelf, and then you go to the cashier to pay for it. + +```move +{{#include ../../../packages/samples/sources/programmability/hot-potato-pattern.move:phone_shop}} +``` + +This decoupling technique allows separating the purchase logic from the payment logic, making the +code more modular and easier to maintain. The `Ticket` could be split into its own module, providing +a basic interface for the payment, and the shop implementation could be extended to support other +goods without changing the payment logic. + +### Compositional Patterns + +Hot potato can be used to link together different modules in a compositional way. Its module may +define ways to interact with the hot potato, for example, stamp it with a type signature, or to +extract some information from it. This way, the hot potato can be passed between different modules, +and even different packages within the same transaction. + +The most important compositional pattern is the [Request Pattern](./request-pattern.md), which we +will cover in the next section. + +### Usage in the Sui Framework + +The pattern is used in various forms in the Sui Framework. Here are some examples: + +- `sui::borrow` - uses hot potato to ensure that the borrowed value is returned to the correct + container. +- `sui::transfer_policy` - defines a `TransferRequest` - a hot potato which can only be consumed if + all conditions are met. +- `sui::token` - in the Closed Loop Token system, an `ActionRequest` carries the information about + the performed action and collects approvals similarly to `TransferRequest`. + +## Summary + +- A hot potato is a struct without abilities, it must come with a way to create and destroy it. +- Hot potatoes are used to ensure that some action is taken before the transaction ends, similar to + a callback. +- Most common use cases for hot potato are borrowing, flash loans, variable-path execution, and + compositional patterns. diff --git a/book/src/programmability/hot-potato.md b/book/src/programmability/hot-potato.md deleted file mode 100644 index 77c45b60..00000000 --- a/book/src/programmability/hot-potato.md +++ /dev/null @@ -1 +0,0 @@ -# Pattern: Hot Potato diff --git a/packages/hello_world/Move.toml b/packages/hello_world/Move.toml index 201de173..58315a87 100644 --- a/packages/hello_world/Move.toml +++ b/packages/hello_world/Move.toml @@ -5,7 +5,7 @@ edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] [dependencies] -Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } +Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/mainnet" } # For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. # Revision can be a branch, a tag, and a commit hash. @@ -34,4 +34,3 @@ hello_world = "0x0" # The dev-addresses section allows overwriting named addresses for the `--test` # and `--dev` modes. # alice = "0xB0B" - diff --git a/packages/reference/Move.toml b/packages/reference/Move.toml index 8519c7fd..dee484ac 100644 --- a/packages/reference/Move.toml +++ b/packages/reference/Move.toml @@ -5,7 +5,7 @@ edition = "2024.alpha" [dependencies.Sui] git = "https://github.com/MystenLabs/sui.git" subdir = "crates/sui-framework/packages/sui-framework" -rev = "main" +rev = "framework/mainnet" [addresses] ref = "0x0" diff --git a/packages/samples/Move.toml b/packages/samples/Move.toml index 87e5023f..76d70c68 100644 --- a/packages/samples/Move.toml +++ b/packages/samples/Move.toml @@ -5,7 +5,7 @@ edition = "2024.beta" [dependencies.Sui] git = "https://github.com/MystenLabs/sui.git" subdir = "crates/sui-framework/packages/sui-framework" -rev = "main" +rev = "framework/mainnet" [addresses] book = "0x0" diff --git a/packages/samples/sources/programmability/hot-potato-pattern.move b/packages/samples/sources/programmability/hot-potato-pattern.move new file mode 100644 index 00000000..02b7f056 --- /dev/null +++ b/packages/samples/sources/programmability/hot-potato-pattern.move @@ -0,0 +1,98 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[allow(unused_variable)] +module book::hot_potato_pattern { + +// ANCHOR: definition +public struct Request {} +// ANCHOR_END: definition + +// ANCHOR: new_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; +} +// ANCHOR_END: new_request + +} + +module book::container_borrow { + +// ANCHOR: container_borrow +/// 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 has key { + id: UID, + value: Option, +} + +/// 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 module that allows borrowing the value from the container. +public fun borrow_val(container: &mut Container): (T, Promise) { + assert!(container.value.is_some()); + 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( + container: &mut Container, value: T, promise: Promise +) { + let Promise { id, container_id } = promise; + assert!(object::id(container) == container_id); + assert!(object::id(&value) == id); + container.value.fill(value); +} +// ANCHOR_END: container_borrow +} + +module book::phone_shop { + +use sui::coin::Coin; +public struct USD has drop {} +public struct BONUS has drop {} + +// ANCHOR: phone_shop +/// 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` or `SUI`. +public fun pay_in_bonus_points(ticket: Ticket, payment: Coin) { + let Ticket { amount } = ticket; + assert!(payment.value() == amount); + abort 0 // omitting the rest of the function +} + +/// The customer may pay for the `Phone` with `USD`. +public fun pay_in_usd(ticket: Ticket, payment: Coin) { + let Ticket { amount } = ticket; + assert!(payment.value() == amount); + abort 0 // omitting the rest of the function +} +// ANCHOR_END: phone_shop +} diff --git a/packages/todo_list/Move.toml b/packages/todo_list/Move.toml index 0ed50e80..2efcf687 100644 --- a/packages/todo_list/Move.toml +++ b/packages/todo_list/Move.toml @@ -5,7 +5,7 @@ edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] [dependencies] -Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/devnet" } +Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/mainnet" } # For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. # Revision can be a branch, a tag, and a commit hash.