Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main'
Browse files Browse the repository at this point in the history
  • Loading branch information
renshuncui committed Aug 13, 2024
2 parents ed8cf17 + 1bd5b0b commit 74ea51f
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 13 deletions.
2 changes: 1 addition & 1 deletion book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
- [Events](./programmability/events.md)
- [Balance & Coin]() <!-- ./programmability/balance-and-coin.md) -->
- [Sui Framework](./programmability/sui-framework.md)
- [Pattern: Hot Potato]() <!-- ./programmability/hot-potato.md) -->
- [Pattern: Hot Potato](./programmability/hot-potato-pattern.md)
- [Pattern: Request]()
- [Pattern: Object Capability]()
- [Package Upgrades]()<!-- (./programmability/package-upgrades.md) -->
Expand Down
121 changes: 121 additions & 0 deletions book/src/programmability/hot-potato-pattern.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 0 additions & 1 deletion book/src/programmability/hot-potato.md

This file was deleted.

3 changes: 1 addition & 2 deletions packages/hello_world/Move.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move
# authors = ["..."] # e.g., ["Joe Smith ([email protected])", "John Snow ([email protected])"]

[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.
Expand Down Expand Up @@ -34,4 +34,3 @@ hello_world = "0x0"
# The dev-addresses section allows overwriting named addresses for the `--test`
# and `--dev` modes.
# alice = "0xB0B"

2 changes: 1 addition & 1 deletion packages/reference/Move.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion packages/samples/Move.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
98 changes: 98 additions & 0 deletions packages/samples/sources/programmability/hot-potato-pattern.move
Original file line number Diff line number Diff line change
@@ -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<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 module that allows borrowing the value from the container.
public fun borrow_val<T: key + store>(container: &mut Container<T>): (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<T: key + store>(
container: &mut Container<T>, 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<BONUS>) {
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<USD>) {
let Ticket { amount } = ticket;
assert!(payment.value() == amount);
abort 0 // omitting the rest of the function
}
// ANCHOR_END: phone_shop
}
2 changes: 1 addition & 1 deletion packages/todo_list/Move.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move
# authors = ["..."] # e.g., ["Joe Smith ([email protected])", "John Snow ([email protected])"]

[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.
Expand Down
1 change: 1 addition & 0 deletions reference/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- [Labeled Control FLow](control-flow/labeled-control-flow.md)
- [Pattern Matching](control-flow/pattern-matching.md)
- [Functions](functions.md)
- [Macro Functions](functions/macros.md)
- [Structs](structs.md)
- [Enums](enums.md)
- [Constants](constants.md)
Expand Down
2 changes: 1 addition & 1 deletion reference/src/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ module b::other {
Type arguments can be either specified or inferred. Both calls are equivalent.

```move
module aexample {
module a::example {
public fun id<T>(x: T): T { x }
}
Expand Down
10 changes: 5 additions & 5 deletions reference/src/functions/macros.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ A few examples
If the return type is not annotated, it is unit `()` by default.

```move
// the following are euiqvalent
// the following are equivalent
|&mut vector<u8>, u64|
|&mut vector<u8>, u64| -> ()
```
Expand Down Expand Up @@ -243,7 +243,7 @@ In this case, `$T` must be instantiated with a single type, but inference finds
bound to both `u8` and `u16`.

There is a tradeoff however, as the `_` type conveys less meaning and intention for the caller.
Consider `map` macro from above redeclared with `_` instead of `$T` and `$U`.
Consider `map` macro from above re-declared with `_` instead of `$T` and `$U`.

```move
macro fun map($v: vector<_>, $f: |_| -> _): vector<_> {
Expand Down Expand Up @@ -340,7 +340,7 @@ The short answer is, no. `macro` functions are
lambdas will not accidentally capture variables from another scope.

The compiler does this by associating a unique number with each scope. When the `macro` is expanded,
the macro body gets its own scope. Additionally, the arguments are rescoped on each usage.
the macro body gets its own scope. Additionally, the arguments are re-scoped on each usage.

Modifying the `dup` macro to use `x` instead of `a`

Expand Down Expand Up @@ -368,7 +368,7 @@ let sum = {
This is an approximation of the compiler's internal representation, some details are omitted for the
simplicity of this example.

And each usage of an argument is rescoped so that the different usages do not conflict.
And each usage of an argument is re-scoped so that the different usages do not conflict.

```move
macro fun apply_twice($f: |u64| -> u64, $x: u64): u64 {
Expand Down Expand Up @@ -560,7 +560,7 @@ expanded, which forces the evaluation and thus the abort.
### Parameter Limitations

The parameters of a `macro` function must always be used as expressions. They cannot be used in
sutations where the argument might be re-interpreted. For example, the following is not allowed
situations where the argument might be re-interpreted. For example, the following is not allowed

```move
macro fun no($x: _): _ {
Expand Down

0 comments on commit 74ea51f

Please sign in to comment.