From f022f48155f93846eb8a7b7f38de535a16bc0ad0 Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Tue, 5 Mar 2024 09:43:24 +0700 Subject: [PATCH 01/16] feat: adding ptb --- README.md | 4 ++ unit-five/example_projects/kiosk/Move.toml | 38 +++++++++++++++++++ .../1_programmable_transaction_block.md | 27 +++++++++++++ unit-five/readme.md | 0 4 files changed, 69 insertions(+) create mode 100644 unit-five/example_projects/kiosk/Move.toml create mode 100644 unit-five/lessons/1_programmable_transaction_block.md create mode 100644 unit-five/readme.md diff --git a/README.md b/README.md index bb7985e..5c82c1b 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,10 @@ Introductory course to the Move language maintained by [Sui Foundation](https:// - [Heterogeneous Collections](./unit-four/lessons/3_heterogeneous_collections.md) - [Marketplace Contract](./unit-four/lessons/4_marketplace_contract.md) - [Deployment and Testing](./unit-four/lessons/5_deployment_and_testing.md) +- **Unit Five: Sui Kiosk** + - [Programmable Transaction Block](./unit-five/lessons/1_programmable_transaction_block.md) + - [Hot Potato Design Pattern][./unit-five/lessons/2_hot_potato_design_pattern.md] + - [Sui Kiosk]() - **Advanced Topics** - [BCS Encoding](./advanced-topics/BCS_encoding/lessons/BCS_encoding.md) diff --git a/unit-five/example_projects/kiosk/Move.toml b/unit-five/example_projects/kiosk/Move.toml new file mode 100644 index 0000000..dda26cf --- /dev/null +++ b/unit-five/example_projects/kiosk/Move.toml @@ -0,0 +1,38 @@ +[package] +name = "kiosk" + +# edition = "2024.alpha" # To use the Move 2024 edition, currently in alpha +# license = "" # e.g., "MIT", "GPL", "Apache 2.0" +# 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" } + +# For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. +# Revision can be a branch, a tag, and a commit hash. +# MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } + +# For local dependencies use `local = path`. Path is relative to the package root +# Local = { local = "../path/to" } + +# To resolve a version conflict and force a specific version for dependency +# override use `override = true` +# Override = { local = "../conflicting/version", override = true } + +[addresses] +kiosk = "0x0" + +# Named addresses will be accessible in Move as `@name`. They're also exported: +# for example, `std = "0x1"` is exported by the Standard Library. +# alice = "0xA11CE" + +[dev-dependencies] +# The dev-dependencies section allows overriding dependencies for `--test` and +# `--dev` modes. You can introduce test-only dependencies here. +# Local = { local = "../path/to/dev-build" } + +[dev-addresses] +# The dev-addresses section allows overwriting named addresses for the `--test` +# and `--dev` modes. +# alice = "0xB0B" + diff --git a/unit-five/lessons/1_programmable_transaction_block.md b/unit-five/lessons/1_programmable_transaction_block.md new file mode 100644 index 0000000..16d9758 --- /dev/null +++ b/unit-five/lessons/1_programmable_transaction_block.md @@ -0,0 +1,27 @@ +# Programmable Transaction Block (PTB) + +Before we get into **Sui Kiosk**, it's neccessary to learn about Programmable Transaction Block (PTB) and how it helps us to seamlessly fulfill Kiosk usage flow + +## Introduction + +Most of us, more or less, have run into the situation where we want to batch a number of smaller transactions in order into a larger unit and submit one single transaction execution to the blockchain. In traditional blockchain, it was not feasible, and we need workarounds to make this work, the common solutions are: +- Submit the transactions subsequently one by one. This way works fine but the performance of your dApps is demoted significantly as you need to wait one transaction to be finalized before you can use their outputs for the next transaction in line. Moreover, the gas fee will not be a pleasant for the end-users +- Create a new smart contract and a wrapper function to execute other functions from the same or different smart contracts. This approach may speed up your application and consume less gas fee but in return, reduce the developer experience as every new business use case might need a new wrapper function. + +That’s why we introduce Programmable Transaction Block (PTB). + +## Features + +PTB is a built-in feature and supported natively by Sui Network and Sui VM. On Sui, a transaction (block) by default is a Programmable Transaction Block (PTB). PTB is a powerful tool enhancing developers with scalalability and composability: +- Each PTB is composed of multiple individual commands chaining together in order. One command that we will use most of the time is `MoveCall`. For other commands, please refer to the [documentation here](https://docs.sui.io/concepts/transactions/prog-txn-blocks#executing-a-transaction-command). +- When the transaction is executed, the commands are executed in the order they are defined when building the PTB. The outputs of one transaction command can be used as inputs for any subsequent commands. +- Sui guarantees the atomicity of a PTB by applying the effects of all commands in the transaction (block) at the end of the transaction. If one command fails, the entire block fails and effects will not take place. +- Each PTB can hold up to 1024 unique operations. This allows cheaper gas fee and faster execution compared to executng 1024 individual transactions in other traditional blockchains. + +*💡Note: Refer to [documentation here](https://docs.sui.io/concepts/transactions/prog-txn-blocks) for full details on PTB* + +## Usage + +There are several ways we can use to build and execute a PTB: +- We already learned how to use the CLI `sui client call` to execute a single smart contract function. Behind the scenes, it is implemented using PTB with single `MoveCall` command. To build a PTB with full functionality, please use the CLI `sui client ptb` and refer to its [usage here](https://docs.sui.io/references/cli/ptb). +- Use the Sui SDK: [Sui Typescript SDK](https://sdk.mystenlabs.com/typescript), [Sui Rust SDK](https://docs.sui.io/references/rust-sdk). diff --git a/unit-five/readme.md b/unit-five/readme.md new file mode 100644 index 0000000..e69de29 From 1f37c7381c66deabc1ef6b2b3783b9eb0b4d7eb2 Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Tue, 5 Mar 2024 09:43:24 +0700 Subject: [PATCH 02/16] feat: adding ptb --- unit-five/lessons/1_programmable_transaction_block.md | 1 + 1 file changed, 1 insertion(+) diff --git a/unit-five/lessons/1_programmable_transaction_block.md b/unit-five/lessons/1_programmable_transaction_block.md index 16d9758..730f0b4 100644 --- a/unit-five/lessons/1_programmable_transaction_block.md +++ b/unit-five/lessons/1_programmable_transaction_block.md @@ -17,6 +17,7 @@ PTB is a built-in feature and supported natively by Sui Network and Sui VM. On S - When the transaction is executed, the commands are executed in the order they are defined when building the PTB. The outputs of one transaction command can be used as inputs for any subsequent commands. - Sui guarantees the atomicity of a PTB by applying the effects of all commands in the transaction (block) at the end of the transaction. If one command fails, the entire block fails and effects will not take place. - Each PTB can hold up to 1024 unique operations. This allows cheaper gas fee and faster execution compared to executng 1024 individual transactions in other traditional blockchains. +- If the output returned by one command is non-`drop` value. It must be consumed by subsequent commands within the same PTB. Otherwise, the transaction (block) is considered to be failed. *💡Note: Refer to [documentation here](https://docs.sui.io/concepts/transactions/prog-txn-blocks) for full details on PTB* From 25df5555bf8e9ae15e89846eafcab3e5cd958558 Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Wed, 6 Mar 2024 02:04:17 +0700 Subject: [PATCH 03/16] feat: adding hot potato pattern --- .gitignore | 1 + .../example_projects/flashloan/Move.lock | 27 +++++++ .../{kiosk => flashloan}/Move.toml | 6 +- .../flashloan/sources/flashloan.move | 59 +++++++++++++++ unit-five/lessons/2_hot_potato_pattern.md | 73 +++++++++++++++++++ 5 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 unit-five/example_projects/flashloan/Move.lock rename unit-five/example_projects/{kiosk => flashloan}/Move.toml (95%) create mode 100644 unit-five/example_projects/flashloan/sources/flashloan.move create mode 100644 unit-five/lessons/2_hot_potato_pattern.md diff --git a/.gitignore b/.gitignore index 5cf6bb1..803e980 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ unit-three/example_projects/generics/build/ unit-three/example_projects/locked_coin/build/ unit-four/example_projects/collections/build/ unit-four/example_projects/marketplace/build/ +unit-five/example_projects/flashloan/build/ advanced-topics/BCS_encoding/example_projects/bcs_move/build/ advanced-topics/BCS_encoding/example_projects/bcs_js/node_modules/ advanced-topics/closed_loop_token/example_projects/closed_loop_token/build/ diff --git a/unit-five/example_projects/flashloan/Move.lock b/unit-five/example_projects/flashloan/Move.lock new file mode 100644 index 0000000..103438f --- /dev/null +++ b/unit-five/example_projects/flashloan/Move.lock @@ -0,0 +1,27 @@ +# @generated by Move, please check-in and do not edit manually. + +[move] +version = 0 +manifest_digest = "FC84CCD33DE1E9661DA31B49398152B41FB6772CFBCD634619716A9823846811" +deps_digest = "F8BBB0CCB2491CA29A3DF03D6F92277A4F3574266507ACD77214D37ECA3F3082" + +dependencies = [ + { name = "Sui" }, +] + +[[move.package]] +name = "MoveStdlib" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/testnet", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +name = "Sui" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/testnet", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { name = "MoveStdlib" }, +] + +[move.toolchain-version] +compiler-version = "1.20.0" +edition = "legacy" +flavor = "sui" diff --git a/unit-five/example_projects/kiosk/Move.toml b/unit-five/example_projects/flashloan/Move.toml similarity index 95% rename from unit-five/example_projects/kiosk/Move.toml rename to unit-five/example_projects/flashloan/Move.toml index dda26cf..524ad2a 100644 --- a/unit-five/example_projects/kiosk/Move.toml +++ b/unit-five/example_projects/flashloan/Move.toml @@ -1,5 +1,6 @@ [package] -name = "kiosk" +name = "flashloan" +version = "0.0.1" # edition = "2024.alpha" # To use the Move 2024 edition, currently in alpha # license = "" # e.g., "MIT", "GPL", "Apache 2.0" @@ -20,7 +21,8 @@ Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-fram # Override = { local = "../conflicting/version", override = true } [addresses] -kiosk = "0x0" +flashloan = "0x0" +sui = "0x2" # Named addresses will be accessible in Move as `@name`. They're also exported: # for example, `std = "0x1"` is exported by the Standard Library. diff --git a/unit-five/example_projects/flashloan/sources/flashloan.move b/unit-five/example_projects/flashloan/sources/flashloan.move new file mode 100644 index 0000000..5aabb40 --- /dev/null +++ b/unit-five/example_projects/flashloan/sources/flashloan.move @@ -0,0 +1,59 @@ +// Copyright (c) Sui Foundation, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module flashloan::flashloan { + // === Imports === + use sui::sui::SUI; + use sui::coin::{Self, Coin}; + use sui::balance::{Self, Balance}; + use sui::object::{UID}; + use sui::tx_context::{TxContext}; + + // === Errors === + + /// For when the loan amount exceed the pool amount + const ELoanAmountExceedPool: u64 = 0; + /// For when the repay amount do not match the initial loan amount + const ERepayAmountInvalid: u64 = 1; + + // === Structs === + + /// A "shared" loan pool. + /// For demonstration purpose, we assume the loan pool only allows SUI. + struct LoanPool has key { + id: UID, + amount: Balance, + } + + /// A loan position. + /// This is a hot potato struct, it enforces the users + /// to repay the loan in the end of the transaction or within the same PTB. + struct Loan { + amount: u64, + } + + // === Public-Mutative Functions === + + /// Function allows users to borrow from the loan pool. + /// It returns the borrowed [`Coin`] and the [`Loan`] position + /// enforcing users to fulfill before the PTB ends. + public fun borrow(pool: &mut LoanPool, amount: u64, ctx: &mut TxContext): (Coin, Loan) { + assert!(amount <= balance::value(&pool.amount), ELoanAmountExceedPool); + + ( + coin::from_balance(balance::split(&mut pool.amount, amount), ctx), + Loan { + amount + } + ) + } + + /// Repay the loan + /// Users must execute this function to ensure the loan is repaid before the transaction ends. + public fun repay(pool: &mut LoanPool, loan: Loan, payment: Coin) { + let Loan { amount } = loan; + assert!(coin::value(&payment) == amount, ERepayAmountInvalid); + + balance::join(&mut pool.amount, coin::into_balance(payment)); + } +} \ No newline at end of file diff --git a/unit-five/lessons/2_hot_potato_pattern.md b/unit-five/lessons/2_hot_potato_pattern.md new file mode 100644 index 0000000..c805604 --- /dev/null +++ b/unit-five/lessons/2_hot_potato_pattern.md @@ -0,0 +1,73 @@ +# Hot Potato Pattern + +A hot potato is a struct that has no capabilities, therefore you can only pack and unpack it in its module. The Hot Potato Pattern leverages the PTB mechanics and is commonly used in cases when the application wants to enforce users to fulfill determined business logic before the transaction ends. In simpler terms, if a hot potato value is returned by the transaction command A, you must consume it in any subsequent command B within the same PTB. The most popular use case of Hot Potato Pattern is flashloan. + +## Type Definitions + +```rust +module flashloan::flashloan { + // === Imports === + use sui::sui::SUI; + use sui::coin::{Self, Coin}; + use sui::balance::{Self, Balance}; + use sui::object::{UID}; + use sui::tx_context::{TxContext}; + + /// For when the loan amount exceed the pool amount + const ELoanAmountExceedPool: u64 = 0; + /// For when the repay amount do not match the initial loan amount + const ERepayAmountInvalid: u64 = 1; + + /// A "shared" loan pool. + /// For demonstration purpose, we assume the loan pool only allows SUI. + struct LoanPool has key { + id: UID, + amount: Balance, + } + + /// A loan position. + /// This is a hot potato struct, it enforces the users + /// to repay the loan in the end of the transaction or within the same PTB. + struct Loan { + amount: u64, + } +} +``` + +We have a `LoanPool` shared object acting as a money vault ready for users to borrow. For simplicity sake, this pool only accepts SUI. Next, we have `Loan` which is a hot potato struct, we will use it to enforce users to repay the loan before transaction ends. `Loan` only has 1 field `amount` which is the borrowed amount. + +## Borrow + +```rust +/// Function allows users to borrow from the loan pool. +/// It returns the borrowed [`Coin`] and the [`Loan`] position +/// enforcing users to fulfill before the PTB ends. +public fun borrow(pool: &mut LoanPool, amount: u64, ctx: &mut TxContext): (Coin, Loan) { + assert!(amount <= balance::value(&pool.amount), ELoanAmountExceedPool); + + ( + coin::from_balance(balance::split(&mut pool.amount, amount), ctx), + Loan { + amount + } + ) +} +``` + +Users can borrow the money from the `LoanPool` by calling `borrow()`. Basically, it will return the `Coin` the users can use as they like for subsequent function calls. A `Loan` hot potato value is also returned. As mentioned previously, the only way to consume the `Loan` is through unpacking it in the functions from the same module. This allows only the application itself has the right to decide how to consume the hot potato, not external parties. + +## Repay + +```rust +/// Repay the loan +/// Users must execute this function to ensure the loan is repaid before the transaction ends. +public fun repay(pool: &mut LoanPool, loan: Loan, payment: Coin) { + let Loan { amount } = loan; + assert!(coin::value(&payment) == amount, ERepayAmountInvalid); + + balance::join(&mut pool.amount, coin::into_balance(payment)); +} +``` + +Users at some point must `repay()` the loan before the PTB ends. We consume the `Loan` by unpacking it, otherwise, you will receive compiler error if you use its fields with direct access `loan.amount` as `Loan` is non-`drop`. After unpacking, we simply use the loan amount to perform valid payment check and update the `LoanPool` accordingly. + From 896bb76bfa438d25e50753b3b823e6e4d6e122ff Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Wed, 6 Mar 2024 02:09:13 +0700 Subject: [PATCH 04/16] feat: fix readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c82c1b..f9aea92 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Introductory course to the Move language maintained by [Sui Foundation](https:// - [Deployment and Testing](./unit-four/lessons/5_deployment_and_testing.md) - **Unit Five: Sui Kiosk** - [Programmable Transaction Block](./unit-five/lessons/1_programmable_transaction_block.md) - - [Hot Potato Design Pattern][./unit-five/lessons/2_hot_potato_design_pattern.md] + - [Hot Potato Design Pattern][./unit-five/lessons/2_hot_potato_pattern.md] - [Sui Kiosk]() - **Advanced Topics** - [BCS Encoding](./advanced-topics/BCS_encoding/lessons/BCS_encoding.md) From df98fe7c9431ee1c273b2bdc666604214ee77cd4 Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Wed, 6 Mar 2024 02:09:49 +0700 Subject: [PATCH 05/16] fix: fix readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f9aea92..23fa566 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ Introductory course to the Move language maintained by [Sui Foundation](https:// - [Deployment and Testing](./unit-four/lessons/5_deployment_and_testing.md) - **Unit Five: Sui Kiosk** - [Programmable Transaction Block](./unit-five/lessons/1_programmable_transaction_block.md) - - [Hot Potato Design Pattern][./unit-five/lessons/2_hot_potato_pattern.md] - - [Sui Kiosk]() + - [Hot Potato Design Pattern](./unit-five/lessons/2_hot_potato_pattern.md) + - [Sui Kiosk](./unit-five/lessons/3_kiosk.md) - **Advanced Topics** - [BCS Encoding](./advanced-topics/BCS_encoding/lessons/BCS_encoding.md) From 716c557fc1583753660b3851b5d91acb9075dd03 Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Wed, 6 Mar 2024 03:36:53 +0700 Subject: [PATCH 06/16] feat: Sui Kiosk (wip) --- .gitignore | 1 + unit-five/example_projects/kiosk/Move.toml | 40 +++++++++++++++++++ .../example_projects/kiosk/sources/kiosk.move | 6 +++ unit-five/lessons/3_kiosk.md | 38 ++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 unit-five/example_projects/kiosk/Move.toml create mode 100644 unit-five/example_projects/kiosk/sources/kiosk.move create mode 100644 unit-five/lessons/3_kiosk.md diff --git a/.gitignore b/.gitignore index 803e980..a37c642 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ unit-three/example_projects/locked_coin/build/ unit-four/example_projects/collections/build/ unit-four/example_projects/marketplace/build/ unit-five/example_projects/flashloan/build/ +unit-five/example_projects/kiosk/build/ advanced-topics/BCS_encoding/example_projects/bcs_move/build/ advanced-topics/BCS_encoding/example_projects/bcs_js/node_modules/ advanced-topics/closed_loop_token/example_projects/closed_loop_token/build/ diff --git a/unit-five/example_projects/kiosk/Move.toml b/unit-five/example_projects/kiosk/Move.toml new file mode 100644 index 0000000..94ca44d --- /dev/null +++ b/unit-five/example_projects/kiosk/Move.toml @@ -0,0 +1,40 @@ +[package] +name = "kiosk" +version = "0.0.1" + +# edition = "2024.alpha" # To use the Move 2024 edition, currently in alpha +# license = "" # e.g., "MIT", "GPL", "Apache 2.0" +# 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" } + +# For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. +# Revision can be a branch, a tag, and a commit hash. +# MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } + +# For local dependencies use `local = path`. Path is relative to the package root +# Local = { local = "../path/to" } + +# To resolve a version conflict and force a specific version for dependency +# override use `override = true` +# Override = { local = "../conflicting/version", override = true } + +[addresses] +kiosk = "0x0" +sui = "0x2" + +# Named addresses will be accessible in Move as `@name`. They're also exported: +# for example, `std = "0x1"` is exported by the Standard Library. +# alice = "0xA11CE" + +[dev-dependencies] +# The dev-dependencies section allows overriding dependencies for `--test` and +# `--dev` modes. You can introduce test-only dependencies here. +# Local = { local = "../path/to/dev-build" } + +[dev-addresses] +# The dev-addresses section allows overwriting named addresses for the `--test` +# and `--dev` modes. +# alice = "0xB0B" + diff --git a/unit-five/example_projects/kiosk/sources/kiosk.move b/unit-five/example_projects/kiosk/sources/kiosk.move new file mode 100644 index 0000000..e3a7905 --- /dev/null +++ b/unit-five/example_projects/kiosk/sources/kiosk.move @@ -0,0 +1,6 @@ +// Copyright (c) Sui Foundation, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module kiosk::kiosk { + +} \ No newline at end of file diff --git a/unit-five/lessons/3_kiosk.md b/unit-five/lessons/3_kiosk.md new file mode 100644 index 0000000..f76d54b --- /dev/null +++ b/unit-five/lessons/3_kiosk.md @@ -0,0 +1,38 @@ +# Sui Kiosk + +Now we have learned the basics of **Programmable Transaction Block** and **Hot Potato Design Pattern**, it is much easier for us to understand the mechanism behind **Sui Kiosk**. Let's get started + +## What is Sui Kiosk? + +We're probably familiar to some sort of kiosks in real life. It can be a stall in a tourist shopping mall selling you merchantdise, apparels or any local souvenirs. It can be in a form of big screen displaying you digital images of the products you're interested in. They may all come with different forms and sizes but they have one common trait: *they sell something and display their wares openly for passersby to browse and engage with* + +**Sui Kiosk** is the digital version of these types of kiosk but for digital assets and collectibles. Sui Kiosk is a *decentralized system for onchain commerce applications on Sui*. Practically, Kiosk is a part of the Sui framework, and it is native to the system and available to everyone out of the box. + +## Why Sui Kiosk? + +Sui Kiosk is created to answer these needs: +- Can we list an item on marketplace and continue using it? +- Is there a way to create a “safe” for collectibles? +- Can we build an onchain system with custom logic for transfer management? +- How to favor creators and guarantee royalties? +- Can we avoid centralization of traditional marketplaces? + +## Main Components + +Sui Kiosk consists these 2 main components: +- `Kiosk` + `KioskOwnerCap`: `Kiosk` is the safe that will store our assets and display them for selling, it is implemented as a shared object allowing interactions between multiple parties. Each `Kiosk` will have a corresponding Kiosk Owner whoever holding the `KioskOwnerCap`. The Kiosk Owner still have the *logical ownership* over their assets even when they are *physically* put into the kiosk. +- `TransferPolicy` + `TransferPolicyCap`: `TransferPolicy` defines the conditions in which the assets can be traded or sold. Each `TransferPolicy` consists a set of *rules*, with each rule specifies the requirements every trade must sastify. Rules can be enabled or disabled from the `TransferPolicy` by whoever owning the `TransferOwnerCap`. Greate example of `TransferPolicy`'s rule is the royalty fees guarantee. + +## Asset States in Sui Kiosk + +Sui Kiosk is a shared object that can store heterogeneous values, such as different sets of asset collectibles. When you add an asset to your kiosk, it has one of the following states: +- `PLACED` - an item is placed inside the Kiosk + +## Sui Kiosk Users + +Sui Kiosk serves these types of users. + +### Kiosk Owner (Seller/KO) + +To become the Kiosk Owner, one must own the `KioskOwnerCap`. To create the Kiosk, we can use the + From 3ba6f124fb4d09c9874e6958e4c7628c24104fef Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Wed, 6 Mar 2024 04:07:04 +0700 Subject: [PATCH 07/16] feat(kiosk): add kiosk users and asset states --- unit-five/lessons/3_kiosk.md | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/unit-five/lessons/3_kiosk.md b/unit-five/lessons/3_kiosk.md index f76d54b..7e95287 100644 --- a/unit-five/lessons/3_kiosk.md +++ b/unit-five/lessons/3_kiosk.md @@ -20,19 +20,35 @@ Sui Kiosk is created to answer these needs: ## Main Components Sui Kiosk consists these 2 main components: -- `Kiosk` + `KioskOwnerCap`: `Kiosk` is the safe that will store our assets and display them for selling, it is implemented as a shared object allowing interactions between multiple parties. Each `Kiosk` will have a corresponding Kiosk Owner whoever holding the `KioskOwnerCap`. The Kiosk Owner still have the *logical ownership* over their assets even when they are *physically* put into the kiosk. +- `Kiosk` + `KioskOwnerCap`: `Kiosk` is the safe that will store our assets and display them for selling, it is implemented as a shared object allowing interactions between multiple parties. Each `Kiosk` will have a corresponding Kiosk Owner whoever holding the `KioskOwnerCap`. The Kiosk Owner still have the *logical ownership* over their assets even when they are *physically* placed in the kiosk. - `TransferPolicy` + `TransferPolicyCap`: `TransferPolicy` defines the conditions in which the assets can be traded or sold. Each `TransferPolicy` consists a set of *rules*, with each rule specifies the requirements every trade must sastify. Rules can be enabled or disabled from the `TransferPolicy` by whoever owning the `TransferOwnerCap`. Greate example of `TransferPolicy`'s rule is the royalty fees guarantee. -## Asset States in Sui Kiosk +## Sui Kiosk Users -Sui Kiosk is a shared object that can store heterogeneous values, such as different sets of asset collectibles. When you add an asset to your kiosk, it has one of the following states: -- `PLACED` - an item is placed inside the Kiosk +Sui Kiosk use-cases is centered around these 3 types of users: +- Kiosk Owner (Seller/KO): One must own the `KioskOwnerCap` to become the Kiosk Owner. KO can: + - Place their assets in kiosk. + - Withdraw the assets in kiosk if they're not *locked*. + - List assets for sale. + - Withdraw profits from sales. + - Borrow and mutate owned assets in kiosk. +- Buyer: Buyer can be anyone who's willing to purchase the listed items. The buyers must satisfy the `TransferPolicy` for the trade to be considered successful. +- Creator: Creator is a party that creates and controls the `TransferPolicy` for a single type. For example, authors of SuiFrens collectibles are the creators of `SuiFren` type and act as creators in the Sui Kiosk system. Creators can: + - Set any rules for trades. + - Set multiple tracks of rules. + - Enable or disable trades at any moment with a policy. + - Enforce policies (eg royalties) on all trades. + - All operations are affected immediately and globally. -## Sui Kiosk Users +## Asset States in Sui Kiosk + +When you add an asset to your kiosk, it has one of the following states: +- `PLACED` - an item is placed inside the kiosk. The Kiosk Owner can withdraw it and use it directly, borrow it (mutably or immutably), or list an item for sale. +- `LOCKED` - an item is placed and locked in the kiosk. The Kiosk Owner can't withdraw a *locked* item from kiosk, but you can borrow it mutably and list it for sale. +- `LISTED` - an item in the kiosk that is listed for sale. The Kiosk Owner can’t modify an item while listed, but you can borrow it immutably or delist it, which returns it to its previous state. -Sui Kiosk serves these types of users. +*💡Note: there is another state called `LISTED EXCLUSIVELY`, which is not covered in this unit and will be covered in the future in advanced section* -### Kiosk Owner (Seller/KO) +## -To become the Kiosk Owner, one must own the `KioskOwnerCap`. To create the Kiosk, we can use the From 81b3f5382387867e60ea65615d29748364c9f1c0 Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Wed, 6 Mar 2024 06:44:45 +0700 Subject: [PATCH 08/16] feat: add kiosk policy --- unit-five/example_projects/kiosk/Move.lock | 27 ++ .../kiosk/sources/dummy_policy.move | 50 ++++ .../kiosk/sources/fixed_royalty_rule.move | 77 ++++++ .../example_projects/kiosk/sources/kiosk.move | 61 ++++- unit-five/lessons/3_kiosk.md | 241 ++++++++++++++++-- 5 files changed, 438 insertions(+), 18 deletions(-) create mode 100644 unit-five/example_projects/kiosk/Move.lock create mode 100644 unit-five/example_projects/kiosk/sources/dummy_policy.move create mode 100644 unit-five/example_projects/kiosk/sources/fixed_royalty_rule.move diff --git a/unit-five/example_projects/kiosk/Move.lock b/unit-five/example_projects/kiosk/Move.lock new file mode 100644 index 0000000..cd31626 --- /dev/null +++ b/unit-five/example_projects/kiosk/Move.lock @@ -0,0 +1,27 @@ +# @generated by Move, please check-in and do not edit manually. + +[move] +version = 0 +manifest_digest = "1FF626947D27118D75E5892ECC965B6EA5D58EF40C92513A237A9F1A2B5F5DDB" +deps_digest = "F8BBB0CCB2491CA29A3DF03D6F92277A4F3574266507ACD77214D37ECA3F3082" + +dependencies = [ + { name = "Sui" }, +] + +[[move.package]] +name = "MoveStdlib" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/testnet", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +name = "Sui" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/testnet", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { name = "MoveStdlib" }, +] + +[move.toolchain-version] +compiler-version = "1.20.0" +edition = "legacy" +flavor = "sui" diff --git a/unit-five/example_projects/kiosk/sources/dummy_policy.move b/unit-five/example_projects/kiosk/sources/dummy_policy.move new file mode 100644 index 0000000..e8f8fe7 --- /dev/null +++ b/unit-five/example_projects/kiosk/sources/dummy_policy.move @@ -0,0 +1,50 @@ +// Copyright (c) Sui Foundation, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// The code is taken here https://github.com/MystenLabs/apps/blob/main/kiosk/docs/creating_a_rule_guide.md#rule-structure-dummy + +module kiosk::dummy_rule { + use sui::coin::Coin; + use sui::sui::SUI; + use sui::transfer_policy::{ + Self as policy, + TransferPolicy, + TransferPolicyCap, + TransferRequest + }; + + /// The Rule Witness; has no fields and is used as a + /// static authorization method for the rule. + struct Rule has drop {} + + /// Configuration struct with any fields (as long as it + /// has `drop`). Managed by the Rule module. + struct Config has store, drop {} + + /// Function that adds a Rule to the `TransferPolicy`. + /// Requires `TransferPolicyCap` to make sure the rules are + /// added only by the publisher of T. + public fun set( + policy: &mut TransferPolicy, + cap: &TransferPolicyCap + ) { + policy::add_rule(Rule {}, policy, cap, Config {}) + } + + /// Action function - perform a certain action (any, really) + /// and pass in the `TransferRequest` so it gets the Receipt. + /// Receipt is a Rule Witness, so there's no way to create + /// it anywhere else but in this module. + /// + /// This example also illustrates that Rules can add Coin + /// to the balance of the TransferPolicy allowing creators to + /// collect fees. + public fun pay( + policy: &mut TransferPolicy, + request: &mut TransferRequest, + payment: Coin + ) { + policy::add_to_balance(Rule {}, policy, payment); + policy::add_receipt(Rule {}, request); + } +} \ No newline at end of file diff --git a/unit-five/example_projects/kiosk/sources/fixed_royalty_rule.move b/unit-five/example_projects/kiosk/sources/fixed_royalty_rule.move new file mode 100644 index 0000000..52b5a9e --- /dev/null +++ b/unit-five/example_projects/kiosk/sources/fixed_royalty_rule.move @@ -0,0 +1,77 @@ +// Copyright (c) Sui Foundation, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// The code is modified from here https://github.com/MystenLabs/apps/blob/main/kiosk/sources/rules/royalty_rule.move + +module kiosk::fixed_royalty_rule { + use sui::sui::SUI; + use sui::coin::{Self, Coin}; + use sui::transfer_policy::{ + Self as policy, + TransferPolicy, + TransferPolicyCap, + TransferRequest + }; + + /// The `amount_bp` passed is more than 100%. + const EIncorrectArgument: u64 = 0; + /// The `Coin` used for payment is not enough to cover the fee. + const EInsufficientAmount: u64 = 1; + + /// Max value for the `amount_bp`. + const MAX_BPS: u16 = 10_000; + + /// The Rule Witness to authorize the policy + struct Rule has drop {} + + /// Configuration for the Rule + struct Config has store, drop { + /// Percentage of the transfer amount to be paid as royalty fee + amount_bp: u16, + /// This is used as royalty fee if the calculated fee is smaller than `min_amount` + min_amount: u64, + } + + /// Function that adds a Rule to the `TransferPolicy`. + /// Requires `TransferPolicyCap` to make sure the rules are + /// added only by the publisher of T. + public fun add( + policy: &mut TransferPolicy, + cap: &TransferPolicyCap, + amount_bp: u16, + min_amount: u64 + + ) { + assert!(amount_bp <= MAX_BPS, EIncorrectArgument); + policy::add_rule(Rule {}, policy, cap, Config { amount_bp, min_amount }) + } + + /// Buyer action: Pay the royalty fee for the transfer. + public fun pay( + policy: &mut TransferPolicy, + request: &mut TransferRequest, + payment: Coin + ) { + let paid = policy::paid(request); + let amount = fee_amount(policy, paid); + + assert!(coin::value(&payment) == amount, EInsufficientAmount); + + policy::add_to_balance(Rule {}, policy, payment); + policy::add_receipt(Rule {}, request) + } + + /// Helper function to calculate the amount to be paid for the transfer. + /// Can be used dry-runned to estimate the fee amount based on the Kiosk listing price. + public fun fee_amount(policy: &TransferPolicy, paid: u64): u64 { + let config: &Config = policy::get_rule(Rule {}, policy); + let amount = (((paid as u128) * (config.amount_bp as u128) / 10_000) as u64); + + // If the amount is less than the minimum, use the minimum + if (amount < config.min_amount) { + amount = config.min_amount + }; + + amount + } +} \ No newline at end of file diff --git a/unit-five/example_projects/kiosk/sources/kiosk.move b/unit-five/example_projects/kiosk/sources/kiosk.move index e3a7905..1c2539a 100644 --- a/unit-five/example_projects/kiosk/sources/kiosk.move +++ b/unit-five/example_projects/kiosk/sources/kiosk.move @@ -2,5 +2,64 @@ // SPDX-License-Identifier: Apache-2.0 module kiosk::kiosk { - + use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; + use sui::tx_context::{TxContext, sender}; + use sui::object::{Self, UID}; + use sui::coin::{Self, Coin}; + use sui::sui::{SUI}; + use sui::transfer_policy::{Self, TransferRequest, TransferPolicy, TransferPolicyCap}; + use sui::package::{Self, Publisher}; + use sui::transfer::{Self}; + + struct TShirt has key, store { + id: UID, + } + + struct KIOSK has drop {} + + fun init(otw: KIOSK, ctx: &mut TxContext) { + let publisher = package::claim(otw, ctx); + transfer::public_transfer(publisher, sender(ctx)); + } + + public fun new_tshirt(ctx: &mut TxContext): TShirt { + TShirt { + id: object::new(ctx), + } + } + + /// Create new kiosk + public fun new_kiosk(ctx: &mut TxContext): (Kiosk, KioskOwnerCap) { + kiosk::new(ctx) + } + + /// Place item inside Kiosk + public fun place(kiosk: &mut Kiosk, cap: &KioskOwnerCap, item: TShirt) { + kiosk::place(kiosk, cap, item) + } + + /// Withdraw item from Kiosk + public fun withdraw(kiosk: &mut Kiosk, cap: &KioskOwnerCap, item_id: object::ID): TShirt { + kiosk::take(kiosk, cap, item_id) + } + + /// List item for sale + public fun list(kiosk: &mut Kiosk, cap: &KioskOwnerCap, item_id: object::ID, price: u64) { + kiosk::list(kiosk, cap, item_id, price) + } + + /// Buy listed item + public fun buy(kiosk: &mut Kiosk, item_id: object::ID, payment: Coin): (TShirt, TransferRequest){ + kiosk::purchase(kiosk, item_id, payment) + } + + /// Confirm the TransferRequest + public fun confirm_request(policy: &TransferPolicy, req: TransferRequest) { + transfer_policy::confirm_request(policy, req); + } + + /// Create new policy for type `T` + public fun new_policy(publisher: &Publisher, ctx: &mut TxContext): (TransferPolicy, TransferPolicyCap) { + transfer_policy::new(publisher, ctx) + } } \ No newline at end of file diff --git a/unit-five/lessons/3_kiosk.md b/unit-five/lessons/3_kiosk.md index 7e95287..17023a1 100644 --- a/unit-five/lessons/3_kiosk.md +++ b/unit-five/lessons/3_kiosk.md @@ -4,13 +4,14 @@ Now we have learned the basics of **Programmable Transaction Block** and **Hot P ## What is Sui Kiosk? -We're probably familiar to some sort of kiosks in real life. It can be a stall in a tourist shopping mall selling you merchantdise, apparels or any local souvenirs. It can be in a form of big screen displaying you digital images of the products you're interested in. They may all come with different forms and sizes but they have one common trait: *they sell something and display their wares openly for passersby to browse and engage with* +We're probably familiar to some sort of kiosks in real life. It can be a stall in a tourist shopping mall selling you merchantdise, apparels or any local souvenirs. It can be in a form of big screen displaying you digital images of the products you're interested in. They may all come with different forms and sizes but they have one common trait: _they sell something and display their wares openly for passersby to browse and engage with_ -**Sui Kiosk** is the digital version of these types of kiosk but for digital assets and collectibles. Sui Kiosk is a *decentralized system for onchain commerce applications on Sui*. Practically, Kiosk is a part of the Sui framework, and it is native to the system and available to everyone out of the box. +**Sui Kiosk** is the digital version of these types of kiosk but for digital assets and collectibles. Sui Kiosk is a _decentralized system for onchain commerce applications on Sui_. Practically, Kiosk is a part of the Sui framework, and it is native to the system and available to everyone out of the box. ## Why Sui Kiosk? Sui Kiosk is created to answer these needs: + - Can we list an item on marketplace and continue using it? - Is there a way to create a “safe” for collectibles? - Can we build an onchain system with custom logic for transfer management? @@ -20,35 +21,241 @@ Sui Kiosk is created to answer these needs: ## Main Components Sui Kiosk consists these 2 main components: -- `Kiosk` + `KioskOwnerCap`: `Kiosk` is the safe that will store our assets and display them for selling, it is implemented as a shared object allowing interactions between multiple parties. Each `Kiosk` will have a corresponding Kiosk Owner whoever holding the `KioskOwnerCap`. The Kiosk Owner still have the *logical ownership* over their assets even when they are *physically* placed in the kiosk. -- `TransferPolicy` + `TransferPolicyCap`: `TransferPolicy` defines the conditions in which the assets can be traded or sold. Each `TransferPolicy` consists a set of *rules*, with each rule specifies the requirements every trade must sastify. Rules can be enabled or disabled from the `TransferPolicy` by whoever owning the `TransferOwnerCap`. Greate example of `TransferPolicy`'s rule is the royalty fees guarantee. + +- `Kiosk` + `KioskOwnerCap`: `Kiosk` is the safe that will store our assets and display them for selling, it is implemented as a shared object allowing interactions between multiple parties. Each `Kiosk` will have a corresponding Kiosk Owner whoever holding the `KioskOwnerCap`. The Kiosk Owner still have the _logical ownership_ over their assets even when they are _physically_ placed in the kiosk. +- `TransferPolicy` + `TransferPolicyCap`: `TransferPolicy` is a shared object defines the conditions in which the assets can be traded or sold. Each `TransferPolicy` consists a set of _rules_, with each rule specifies the requirements every trade must sastify. Rules can be enabled or disabled from the `TransferPolicy` by whoever owning the `TransferOwnerCap`. Greate example of `TransferPolicy`'s rule is the royalty fees guarantee. ## Sui Kiosk Users Sui Kiosk use-cases is centered around these 3 types of users: + - Kiosk Owner (Seller/KO): One must own the `KioskOwnerCap` to become the Kiosk Owner. KO can: - - Place their assets in kiosk. - - Withdraw the assets in kiosk if they're not *locked*. - - List assets for sale. - - Withdraw profits from sales. - - Borrow and mutate owned assets in kiosk. + - Place their assets in kiosk. + - Withdraw the assets in kiosk if they're not _locked_. + - List assets for sale. + - Withdraw profits from sales. + - Borrow and mutate owned assets in kiosk. - Buyer: Buyer can be anyone who's willing to purchase the listed items. The buyers must satisfy the `TransferPolicy` for the trade to be considered successful. - Creator: Creator is a party that creates and controls the `TransferPolicy` for a single type. For example, authors of SuiFrens collectibles are the creators of `SuiFren` type and act as creators in the Sui Kiosk system. Creators can: - - Set any rules for trades. - - Set multiple tracks of rules. - - Enable or disable trades at any moment with a policy. - - Enforce policies (eg royalties) on all trades. - - All operations are affected immediately and globally. + - Set any rules for trades. + - Set multiple tracks of rules. + - Enable or disable trades at any moment with a policy. + - Enforce policies (eg royalties) on all trades. + - All operations are affected immediately and globally. ## Asset States in Sui Kiosk When you add an asset to your kiosk, it has one of the following states: + - `PLACED` - an item is placed inside the kiosk. The Kiosk Owner can withdraw it and use it directly, borrow it (mutably or immutably), or list an item for sale. -- `LOCKED` - an item is placed and locked in the kiosk. The Kiosk Owner can't withdraw a *locked* item from kiosk, but you can borrow it mutably and list it for sale. +- `LOCKED` - an item is placed and locked in the kiosk. The Kiosk Owner can't withdraw a _locked_ item from kiosk, but you can borrow it mutably and list it for sale. - `LISTED` - an item in the kiosk that is listed for sale. The Kiosk Owner can’t modify an item while listed, but you can borrow it immutably or delist it, which returns it to its previous state. -*💡Note: there is another state called `LISTED EXCLUSIVELY`, which is not covered in this unit and will be covered in the future in advanced section* +_💡Note: there is another state called `LISTED EXCLUSIVELY`, which is not covered in this unit and will be covered in the future in advanced section_ + +## Sui Kiosk Usage + +### Create Kiosk + +```rust +module kiosk::kiosk { + use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; + use sui::tx_context::{TxContext}; + + /// Create new kiosk + public fun new_kiosk(ctx: &mut TxContext): (Kiosk, KioskOwnerCap) { + kiosk::new(ctx) + } +} +``` + +There are 2 ways to create a new kiosk: + +- Use `kiosk::new()` to create new kiosk but we have to make the `Kiosk` shared object and transfer the `KioskOwnerCap` to the sender ourselves by using `sui::transfer` in the same PTB. +- Use `kiosk::default()` to automatically do all above steps for us. However, remeber that `kiosk::default()` is an entry function, so we can't include other calls in the same PTB. + +_💡Note: Kiosk is heterogenous collection by default so that's why it doesn't need type parameter for their items_ + +### Place Item inside Kiosk + +```rust +struct TShirt has key, store { + id: UID, +} + +public fun new_tshirt(ctx: &mut TxContext): TShirt { + TShirt { + id: object::new(ctx), + } +} + +/// Place item inside kiosk +public fun place(kiosk: &mut Kiosk, cap: &KioskOwnerCap, item: TShirt) { + kiosk::place(kiosk, cap, item) +} +``` + +We can use `kiosk::place()` API to place an item inside kiosk. Remember that only the Kiosk Owner can have access to this API. + +### Withdraw Item from Kiosk + +```rust +/// Withdraw item from Kiosk +public fun withdraw(kiosk: &mut Kiosk, cap: &KioskOwnerCap, item_id: object::ID): TShirt { + kiosk::take(kiosk, cap, item_id) +} +``` + +We can use `kiosk::take()` API to withdraw an item from kiosk. Remember that only the Kiosk Owner can have access to this API. + +### List Item for Sale + +```rust +/// List item for sale +public fun list(kiosk: &mut Kiosk, cap: &KioskOwnerCap, item_id: object::ID, price: u64) { + kiosk::list(kiosk, cap, item_id, price) +} +``` + +We can use `kiosk::list()` API to list an item for sale. Remember that only the Kiosk Owner can have access to this API. + +### Buy Item from Kiosk + +```rust +use sui::transfer_policy::{Self, TransferRequest, TransferPolicy}; + +/// Buy listed item +public fun buy(kiosk: &mut Kiosk, item_id: object::ID, payment: Coin): (TShirt, TransferRequest){ + kiosk::purchase(kiosk, item_id, payment) +} + +/// Confirm the TransferRequest +public fun confirm_request(policy: &TransferPolicy, req: TransferRequest) { + transfer_policy::confirm_request(policy, req); +} +``` + +When buyers buy the asset by using `kiosk::purchase()` API, an item is returned alongside with a `TransferRequest`. `TransferRequest` is a hot potato forcing us to consume it through `transfer_policy::confirm_request()`. `confirm_request()`'s job is to verify whether all the rules configured and enabled in the `TransferPolicy` are complied by the users. If one of the enabled rules are not satisfied, then `confirm_request()` throws error leading to the failure of the transaction. As a consequence, the item is not under your ownership even if you tried to transfer the item to your account before `confirm_request()`. + +_💡Note: The users must compose a PTB with all necessary calls to ensure the TransferRequest is valid before `confirm_request()` call._ + +The flow can be illustrated as follow: + +_Buyer -> `kiosk::purchase()` -> `Item` + `TransferRequest` -> Subsequent calls to fulfill `TransferRequest` -> `transfer_policy::confirm_request()` -> Transfer `Item` under ownership_ + +### Create a `TransferPolicy` + +`TransferPolicy` for type `T` must be created for that type `T` to be tradeable in the Kiosk system. `TransferPolicy` is a shared object acting as a central authority so that everyone can use it to `confirm_request()`. + +```rust +use sui::tx_context::{TxContext, sender}; +use sui::transfer_policy::{Self, TransferRequest, TransferPolicy, TransferPolicyCap}; +use sui::package::{Self, Publisher}; +use sui::transfer::{Self}; + +struct KIOSK has drop {} + +fun init(witness: KIOSK, ctx: &mut TxContext) { + let publisher = package::claim(otw, ctx); + transfer::public_transfer(publisher, sender(ctx)); +} + +/// Create new policy for type `T` +public fun new_policy(publisher: &Publisher, ctx: &mut TxContext): (TransferPolicy, TransferPolicyCap) { + transfer_policy::new(publisher, ctx) +} +``` + +Create a `TransferPolicy` requires the proof of publisher `Publisher` of the module comprising `T`. This ensures only the creator of type `T` can create `TransferPolicy`. There are 2 ways to create the policy: + +- Use `transfer_policy::new()` to create new policy but we have to make the `TransferPolicy` shared object and transfer the `TransferPolicyCap` to the `Publisher` ourselves by using `sui::transfer` in the same PTB. +- Use `transfer_policy::default()` to automatically do all above steps for us. However, remeber that `transfer_policy::default()` is an entry function, so we can't include other calls in the same PTB. + +### Implement Fixed Fee Rule + +`TransferPolicy` doesn't enforce anything without any rule, let's learn how to implement a simple rule in a separated module to enforce users to pay a fixed royalty fee for a trade to succeed. + +*💡Note: There is a standard approach to implement the rules. Please checkout the [rule template here](../example_projects/kiosk/sources/dummy_policy.move)* + +#### Rule Witness & Rule Config +```rust +module kiosk::fixed_royalty_rule { + /// The `amount_bp` passed is more than 100%. + const EIncorrectArgument: u64 = 0; + /// The `Coin` used for payment is not enough to cover the fee. + const EInsufficientAmount: u64 = 1; + + /// Max value for the `amount_bp`. + const MAX_BPS: u16 = 10_000; + + /// The Rule Witness to authorize the policy + struct Rule has drop {} + + /// Configuration for the Rule + struct Config has store, drop { + /// Percentage of the transfer amount to be paid as royalty fee + amount_bp: u16, + /// This is used as royalty fee if the calculated fee is smaller than `min_amount` + min_amount: u64, + } +} +``` + +`Rule` represents a witness type to add to `TransferPolicy`, it helps to identify and distinguish between multiple rules adding to one policy. `Config` is the configuration of the `Rule`, as we implement fixed royaltee fee, the settings should include the percentage we want to deduct out of orignal payment. + +#### Add Rule to TransferPolicy + +```rust +/// Function that adds a Rule to the `TransferPolicy`. +/// Requires `TransferPolicyCap` to make sure the rules are +/// added only by the publisher of T. +public fun add( + policy: &mut TransferPolicy, + cap: &TransferPolicyCap, + amount_bp: u16, + min_amount: u64 + +) { + assert!(amount_bp <= MAX_BPS, EIncorrectArgument); + policy::add_rule(Rule {}, policy, cap, Config { amount_bp, min_amount }) +} +``` + +We use `policy::add_rule()` to add the rule with its configuration to the policy. + +#### Buyers Follow the Rule + +```rust +/// Buyer action: Pay the royalty fee for the transfer. +public fun pay( + policy: &mut TransferPolicy, + request: &mut TransferRequest, + payment: Coin +) { + let paid = policy::paid(request); + let amount = fee_amount(policy, paid); + + assert!(coin::value(&payment) == amount, EInsufficientAmount); + + policy::add_to_balance(Rule {}, policy, payment); + policy::add_receipt(Rule {}, request) +} + +/// Helper function to calculate the amount to be paid for the transfer. +/// Can be used dry-runned to estimate the fee amount based on the Kiosk listing price. +public fun fee_amount(policy: &TransferPolicy, paid: u64): u64 { + let config: &Config = policy::get_rule(Rule {}, policy); + let amount = (((paid as u128) * (config.amount_bp as u128) / 10_000) as u64); + + // If the amount is less than the minimum, use the minimum + if (amount < config.min_amount) { + amount = config.min_amount + }; -## + amount +} +``` +We need a helper `fee_amount()` to calculate the royalty fee given the policy and the payment amount. We use `policy::get_rule()` to enquire the configuration and use it for fee calculation. +`pay()` is a function that users must call themselves to fullfil the `TransferRequest` before `confirm_request()`. `policy::paid()` gives us original payment of the trade embedded in the `TransferRequest`. After royalty fee calculation, we will add the fee to the policy through `policy::add_to_balance()`, any fee collected by the policy is accumulated here and `TransferPolicyCap` owner can withdraw later. Last but not least, we use `policy::add_receipt()` to flag the `TransferRequest` that this rule is passed and ready to be confirmed with `confirm_request()`. \ No newline at end of file From 58dffbf34df7be66126853fdd28985c018a5bd3a Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Mon, 11 Mar 2024 23:58:18 +0700 Subject: [PATCH 09/16] feat: refactor hot potato to exclude ptb --- README.md | 5 +- .../flashloan/sources/flashloan.move | 39 +++++++++++++-- ...ato_pattern.md => 1_hot_potato_pattern.md} | 47 +++++++++++++++++-- unit-five/lessons/{3_kiosk.md => 2_kiosk.md} | 0 ...k.md => programmable_transaction_block.md} | 0 5 files changed, 81 insertions(+), 10 deletions(-) rename unit-five/lessons/{2_hot_potato_pattern.md => 1_hot_potato_pattern.md} (50%) rename unit-five/lessons/{3_kiosk.md => 2_kiosk.md} (100%) rename unit-five/lessons/{1_programmable_transaction_block.md => programmable_transaction_block.md} (100%) diff --git a/README.md b/README.md index 23fa566..6093d3d 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,8 @@ Introductory course to the Move language maintained by [Sui Foundation](https:// - [Marketplace Contract](./unit-four/lessons/4_marketplace_contract.md) - [Deployment and Testing](./unit-four/lessons/5_deployment_and_testing.md) - **Unit Five: Sui Kiosk** - - [Programmable Transaction Block](./unit-five/lessons/1_programmable_transaction_block.md) - - [Hot Potato Design Pattern](./unit-five/lessons/2_hot_potato_pattern.md) - - [Sui Kiosk](./unit-five/lessons/3_kiosk.md) + - [Hot Potato Design Pattern](./unit-five/lessons/1_hot_potato_pattern.md) + - [Sui Kiosk](./unit-five/lessons/2_kiosk.md) - **Advanced Topics** - [BCS Encoding](./advanced-topics/BCS_encoding/lessons/BCS_encoding.md) diff --git a/unit-five/example_projects/flashloan/sources/flashloan.move b/unit-five/example_projects/flashloan/sources/flashloan.move index 5aabb40..aa31f52 100644 --- a/unit-five/example_projects/flashloan/sources/flashloan.move +++ b/unit-five/example_projects/flashloan/sources/flashloan.move @@ -6,7 +6,7 @@ module flashloan::flashloan { use sui::sui::SUI; use sui::coin::{Self, Coin}; use sui::balance::{Self, Balance}; - use sui::object::{UID}; + use sui::object::{Self, UID}; use sui::tx_context::{TxContext}; // === Errors === @@ -27,16 +27,22 @@ module flashloan::flashloan { /// A loan position. /// This is a hot potato struct, it enforces the users - /// to repay the loan in the end of the transaction or within the same PTB. + /// to repay the loan in the end of the transaction. struct Loan { amount: u64, } + /// Example NFT for demonstration purpose + struct NFT has key, store { + id: UID, + price: Balance, + } + // === Public-Mutative Functions === /// Function allows users to borrow from the loan pool. /// It returns the borrowed [`Coin`] and the [`Loan`] position - /// enforcing users to fulfill before the PTB ends. + /// enforcing users to fulfill before the transaction ends. public fun borrow(pool: &mut LoanPool, amount: u64, ctx: &mut TxContext): (Coin, Loan) { assert!(amount <= balance::value(&pool.amount), ELoanAmountExceedPool); @@ -56,4 +62,31 @@ module flashloan::flashloan { balance::join(&mut pool.amount, coin::into_balance(payment)); } + + /// Buy a NFT + public fun buy_nft(payment: Coin, ctx: &mut TxContext): NFT { + NFT { + id: object::new(ctx), + price: coin::into_balance(payment), + } + } + + /// Sell a NFT + public fun sell_nft(nft: NFT, ctx: &mut TxContext): Coin { + let NFT {id, price} = nft; + object::delete(id); + coin::from_balance(price, ctx) + } + + /// Flashloan + public fun flashloan(pool: &mut LoanPool, amount: u64, ctx: &mut TxContext) { + let (loanCoin, loan) = borrow(pool, amount, ctx); + + /// We can call multiple functions in-between `borrow()` and `repay()` to use the loan for our own utility. + /// We demonstrate this behavior by buying a NFT and sell it instantly to repay the debt + let nft = buy_nft(loanCoin, ctx); + let repayCoin = sell_nft(nft, ctx); + + repay(pool, loan, repayCoin); + } } \ No newline at end of file diff --git a/unit-five/lessons/2_hot_potato_pattern.md b/unit-five/lessons/1_hot_potato_pattern.md similarity index 50% rename from unit-five/lessons/2_hot_potato_pattern.md rename to unit-five/lessons/1_hot_potato_pattern.md index c805604..cde15ae 100644 --- a/unit-five/lessons/2_hot_potato_pattern.md +++ b/unit-five/lessons/1_hot_potato_pattern.md @@ -1,6 +1,8 @@ # Hot Potato Pattern -A hot potato is a struct that has no capabilities, therefore you can only pack and unpack it in its module. The Hot Potato Pattern leverages the PTB mechanics and is commonly used in cases when the application wants to enforce users to fulfill determined business logic before the transaction ends. In simpler terms, if a hot potato value is returned by the transaction command A, you must consume it in any subsequent command B within the same PTB. The most popular use case of Hot Potato Pattern is flashloan. +A hot potato is a struct that has no capabilities, therefore you can only pack and unpack it in its module. The Hot Potato Pattern is commonly used in cases when the application wants to enforce users to fulfill determined business logic before the transaction ends. It is also usually used in conjunction with Programmable Transaction Block (PTB). The most popular use case of Hot Potato Pattern is flashloan. + +*💡Note: Read more details about [Programmable Transaction Block (PTB) here](./programmable_transaction_block.md)* ## Type Definitions @@ -27,7 +29,7 @@ module flashloan::flashloan { /// A loan position. /// This is a hot potato struct, it enforces the users - /// to repay the loan in the end of the transaction or within the same PTB. + /// to repay the loan in the end of the transaction. struct Loan { amount: u64, } @@ -41,7 +43,7 @@ We have a `LoanPool` shared object acting as a money vault ready for users to bo ```rust /// Function allows users to borrow from the loan pool. /// It returns the borrowed [`Coin`] and the [`Loan`] position -/// enforcing users to fulfill before the PTB ends. +/// enforcing users to fulfill before the transaction ends. public fun borrow(pool: &mut LoanPool, amount: u64, ctx: &mut TxContext): (Coin, Loan) { assert!(amount <= balance::value(&pool.amount), ELoanAmountExceedPool); @@ -69,5 +71,42 @@ public fun repay(pool: &mut LoanPool, loan: Loan, payment: Coin) { } ``` -Users at some point must `repay()` the loan before the PTB ends. We consume the `Loan` by unpacking it, otherwise, you will receive compiler error if you use its fields with direct access `loan.amount` as `Loan` is non-`drop`. After unpacking, we simply use the loan amount to perform valid payment check and update the `LoanPool` accordingly. +Users at some point must `repay()` the loan before the transaction ends. We consume the `Loan` by unpacking it, otherwise, you will receive compiler error if you use its fields with direct access `loan.amount` as `Loan` is non-`drop`. After unpacking, we simply use the loan amount to perform valid payment check and update the `LoanPool` accordingly. + +## Flashloan + +```rust +/// Example NFT for demonstration purpose +struct NFT has key, store { + id: UID, + price: Balance, +} + +/// Buy a NFT +public fun buy_nft(payment: Coin, ctx: &mut TxContext): NFT { + NFT { + id: object::new(ctx), + price: coin::into_balance(payment), + } +} + +/// Sell a NFT +public fun sell_nft(nft: NFT, ctx: &mut TxContext): Coin { + let NFT {id, price} = nft; + object::delete(id); + coin::from_balance(price, ctx) +} + +/// Flashloan +public fun flashloan(pool: &mut LoanPool, amount: u64, ctx: &mut TxContext) { + let (loanCoin, loan) = borrow(pool, amount, ctx); + + /// We can call multiple functions in-between `borrow()` and `repay()` to use the loan for our own utility. + /// We demonstrate this behavior by buying a NFT and sell it instantly to repay the debt + let nft = buy_nft(loanCoin, ctx); + let repayCoin = sell_nft(nft, ctx); + + repay(pool, loan, repayCoin); +``` +`flashloan()` demonstrates how we can borrow the coin and use it for our own utility before repaying the debt all in one single transaction. Between `borrow()` and `repay()`, we can freely execute any logic using the loan we just borrow. In the example, we simply buy a NFT and then sell it for profit, then, the profit is used to repay the loan. In the worst scenario where you incur a loss instead, and you can't payback the loan, then the transaction fails and no state changes to the blockchain are applied. This is a very powerful pattern as it requires you to satisfy some business logic atomically in one single transaction to prevent leaking invalid application states. \ No newline at end of file diff --git a/unit-five/lessons/3_kiosk.md b/unit-five/lessons/2_kiosk.md similarity index 100% rename from unit-five/lessons/3_kiosk.md rename to unit-five/lessons/2_kiosk.md diff --git a/unit-five/lessons/1_programmable_transaction_block.md b/unit-five/lessons/programmable_transaction_block.md similarity index 100% rename from unit-five/lessons/1_programmable_transaction_block.md rename to unit-five/lessons/programmable_transaction_block.md From e321fa39d7db157f33f72bf06f5cd6f8f9703734 Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Mon, 25 Mar 2024 15:30:58 +0700 Subject: [PATCH 10/16] Revert to keep PTB --- README.md | 5 +- .../flashloan/sources/flashloan.move | 39 ++------------- ...md => 1_programmable_transaction_block.md} | 0 ...ato_pattern.md => 2_hot_potato_pattern.md} | 47 ++----------------- unit-five/lessons/{2_kiosk.md => 3_kiosk.md} | 0 5 files changed, 10 insertions(+), 81 deletions(-) rename unit-five/lessons/{programmable_transaction_block.md => 1_programmable_transaction_block.md} (100%) rename unit-five/lessons/{1_hot_potato_pattern.md => 2_hot_potato_pattern.md} (50%) rename unit-five/lessons/{2_kiosk.md => 3_kiosk.md} (100%) diff --git a/README.md b/README.md index 6093d3d..23fa566 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,9 @@ Introductory course to the Move language maintained by [Sui Foundation](https:// - [Marketplace Contract](./unit-four/lessons/4_marketplace_contract.md) - [Deployment and Testing](./unit-four/lessons/5_deployment_and_testing.md) - **Unit Five: Sui Kiosk** - - [Hot Potato Design Pattern](./unit-five/lessons/1_hot_potato_pattern.md) - - [Sui Kiosk](./unit-five/lessons/2_kiosk.md) + - [Programmable Transaction Block](./unit-five/lessons/1_programmable_transaction_block.md) + - [Hot Potato Design Pattern](./unit-five/lessons/2_hot_potato_pattern.md) + - [Sui Kiosk](./unit-five/lessons/3_kiosk.md) - **Advanced Topics** - [BCS Encoding](./advanced-topics/BCS_encoding/lessons/BCS_encoding.md) diff --git a/unit-five/example_projects/flashloan/sources/flashloan.move b/unit-five/example_projects/flashloan/sources/flashloan.move index aa31f52..5aabb40 100644 --- a/unit-five/example_projects/flashloan/sources/flashloan.move +++ b/unit-five/example_projects/flashloan/sources/flashloan.move @@ -6,7 +6,7 @@ module flashloan::flashloan { use sui::sui::SUI; use sui::coin::{Self, Coin}; use sui::balance::{Self, Balance}; - use sui::object::{Self, UID}; + use sui::object::{UID}; use sui::tx_context::{TxContext}; // === Errors === @@ -27,22 +27,16 @@ module flashloan::flashloan { /// A loan position. /// This is a hot potato struct, it enforces the users - /// to repay the loan in the end of the transaction. + /// to repay the loan in the end of the transaction or within the same PTB. struct Loan { amount: u64, } - /// Example NFT for demonstration purpose - struct NFT has key, store { - id: UID, - price: Balance, - } - // === Public-Mutative Functions === /// Function allows users to borrow from the loan pool. /// It returns the borrowed [`Coin`] and the [`Loan`] position - /// enforcing users to fulfill before the transaction ends. + /// enforcing users to fulfill before the PTB ends. public fun borrow(pool: &mut LoanPool, amount: u64, ctx: &mut TxContext): (Coin, Loan) { assert!(amount <= balance::value(&pool.amount), ELoanAmountExceedPool); @@ -62,31 +56,4 @@ module flashloan::flashloan { balance::join(&mut pool.amount, coin::into_balance(payment)); } - - /// Buy a NFT - public fun buy_nft(payment: Coin, ctx: &mut TxContext): NFT { - NFT { - id: object::new(ctx), - price: coin::into_balance(payment), - } - } - - /// Sell a NFT - public fun sell_nft(nft: NFT, ctx: &mut TxContext): Coin { - let NFT {id, price} = nft; - object::delete(id); - coin::from_balance(price, ctx) - } - - /// Flashloan - public fun flashloan(pool: &mut LoanPool, amount: u64, ctx: &mut TxContext) { - let (loanCoin, loan) = borrow(pool, amount, ctx); - - /// We can call multiple functions in-between `borrow()` and `repay()` to use the loan for our own utility. - /// We demonstrate this behavior by buying a NFT and sell it instantly to repay the debt - let nft = buy_nft(loanCoin, ctx); - let repayCoin = sell_nft(nft, ctx); - - repay(pool, loan, repayCoin); - } } \ No newline at end of file diff --git a/unit-five/lessons/programmable_transaction_block.md b/unit-five/lessons/1_programmable_transaction_block.md similarity index 100% rename from unit-five/lessons/programmable_transaction_block.md rename to unit-five/lessons/1_programmable_transaction_block.md diff --git a/unit-five/lessons/1_hot_potato_pattern.md b/unit-five/lessons/2_hot_potato_pattern.md similarity index 50% rename from unit-five/lessons/1_hot_potato_pattern.md rename to unit-five/lessons/2_hot_potato_pattern.md index cde15ae..c805604 100644 --- a/unit-five/lessons/1_hot_potato_pattern.md +++ b/unit-five/lessons/2_hot_potato_pattern.md @@ -1,8 +1,6 @@ # Hot Potato Pattern -A hot potato is a struct that has no capabilities, therefore you can only pack and unpack it in its module. The Hot Potato Pattern is commonly used in cases when the application wants to enforce users to fulfill determined business logic before the transaction ends. It is also usually used in conjunction with Programmable Transaction Block (PTB). The most popular use case of Hot Potato Pattern is flashloan. - -*💡Note: Read more details about [Programmable Transaction Block (PTB) here](./programmable_transaction_block.md)* +A hot potato is a struct that has no capabilities, therefore you can only pack and unpack it in its module. The Hot Potato Pattern leverages the PTB mechanics and is commonly used in cases when the application wants to enforce users to fulfill determined business logic before the transaction ends. In simpler terms, if a hot potato value is returned by the transaction command A, you must consume it in any subsequent command B within the same PTB. The most popular use case of Hot Potato Pattern is flashloan. ## Type Definitions @@ -29,7 +27,7 @@ module flashloan::flashloan { /// A loan position. /// This is a hot potato struct, it enforces the users - /// to repay the loan in the end of the transaction. + /// to repay the loan in the end of the transaction or within the same PTB. struct Loan { amount: u64, } @@ -43,7 +41,7 @@ We have a `LoanPool` shared object acting as a money vault ready for users to bo ```rust /// Function allows users to borrow from the loan pool. /// It returns the borrowed [`Coin`] and the [`Loan`] position -/// enforcing users to fulfill before the transaction ends. +/// enforcing users to fulfill before the PTB ends. public fun borrow(pool: &mut LoanPool, amount: u64, ctx: &mut TxContext): (Coin, Loan) { assert!(amount <= balance::value(&pool.amount), ELoanAmountExceedPool); @@ -71,42 +69,5 @@ public fun repay(pool: &mut LoanPool, loan: Loan, payment: Coin) { } ``` -Users at some point must `repay()` the loan before the transaction ends. We consume the `Loan` by unpacking it, otherwise, you will receive compiler error if you use its fields with direct access `loan.amount` as `Loan` is non-`drop`. After unpacking, we simply use the loan amount to perform valid payment check and update the `LoanPool` accordingly. - -## Flashloan - -```rust -/// Example NFT for demonstration purpose -struct NFT has key, store { - id: UID, - price: Balance, -} - -/// Buy a NFT -public fun buy_nft(payment: Coin, ctx: &mut TxContext): NFT { - NFT { - id: object::new(ctx), - price: coin::into_balance(payment), - } -} - -/// Sell a NFT -public fun sell_nft(nft: NFT, ctx: &mut TxContext): Coin { - let NFT {id, price} = nft; - object::delete(id); - coin::from_balance(price, ctx) -} - -/// Flashloan -public fun flashloan(pool: &mut LoanPool, amount: u64, ctx: &mut TxContext) { - let (loanCoin, loan) = borrow(pool, amount, ctx); - - /// We can call multiple functions in-between `borrow()` and `repay()` to use the loan for our own utility. - /// We demonstrate this behavior by buying a NFT and sell it instantly to repay the debt - let nft = buy_nft(loanCoin, ctx); - let repayCoin = sell_nft(nft, ctx); - - repay(pool, loan, repayCoin); -``` +Users at some point must `repay()` the loan before the PTB ends. We consume the `Loan` by unpacking it, otherwise, you will receive compiler error if you use its fields with direct access `loan.amount` as `Loan` is non-`drop`. After unpacking, we simply use the loan amount to perform valid payment check and update the `LoanPool` accordingly. -`flashloan()` demonstrates how we can borrow the coin and use it for our own utility before repaying the debt all in one single transaction. Between `borrow()` and `repay()`, we can freely execute any logic using the loan we just borrow. In the example, we simply buy a NFT and then sell it for profit, then, the profit is used to repay the loan. In the worst scenario where you incur a loss instead, and you can't payback the loan, then the transaction fails and no state changes to the blockchain are applied. This is a very powerful pattern as it requires you to satisfy some business logic atomically in one single transaction to prevent leaking invalid application states. \ No newline at end of file diff --git a/unit-five/lessons/2_kiosk.md b/unit-five/lessons/3_kiosk.md similarity index 100% rename from unit-five/lessons/2_kiosk.md rename to unit-five/lessons/3_kiosk.md From b83cf359c323d7f61f2065fc003da8bee7425a61 Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Tue, 26 Mar 2024 19:10:19 +0700 Subject: [PATCH 11/16] feat: add ptb cli example to hot potato --- .../example_projects/flashloan/Move.lock | 2 +- .../flashloan/sources/flashloan.move | 38 +++++++++++- unit-five/lessons/2_hot_potato_pattern.md | 61 +++++++++++++++++++ 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/unit-five/example_projects/flashloan/Move.lock b/unit-five/example_projects/flashloan/Move.lock index 103438f..da55037 100644 --- a/unit-five/example_projects/flashloan/Move.lock +++ b/unit-five/example_projects/flashloan/Move.lock @@ -22,6 +22,6 @@ dependencies = [ ] [move.toolchain-version] -compiler-version = "1.20.0" +compiler-version = "1.21.0" edition = "legacy" flavor = "sui" diff --git a/unit-five/example_projects/flashloan/sources/flashloan.move b/unit-five/example_projects/flashloan/sources/flashloan.move index 5aabb40..f748d1a 100644 --- a/unit-five/example_projects/flashloan/sources/flashloan.move +++ b/unit-five/example_projects/flashloan/sources/flashloan.move @@ -6,8 +6,9 @@ module flashloan::flashloan { use sui::sui::SUI; use sui::coin::{Self, Coin}; use sui::balance::{Self, Balance}; - use sui::object::{UID}; + use sui::object::{Self, UID}; use sui::tx_context::{TxContext}; + use sui::transfer::{Self}; // === Errors === @@ -32,8 +33,26 @@ module flashloan::flashloan { amount: u64, } + /// A dummy NFT to represent the flashloan functionality + struct NFT has key{ + id: UID, + price: Balance, + } + + fun init(ctx: &mut TxContext) { + let pool = LoanPool { + id: object::new(ctx), + amount: balance::zero() + }; + transfer::share_object(pool); + } // === Public-Mutative Functions === + /// Deposit money into loan pool + public fun deposit_pool(pool: &mut LoanPool, deposit: Coin) { + balance::join(&mut pool.amount, coin::into_balance(deposit)); + } + /// Function allows users to borrow from the loan pool. /// It returns the borrowed [`Coin`] and the [`Loan`] position /// enforcing users to fulfill before the PTB ends. @@ -56,4 +75,19 @@ module flashloan::flashloan { balance::join(&mut pool.amount, coin::into_balance(payment)); } -} \ No newline at end of file + + /// Mint NFT + public fun mint_nft(payment: Coin, ctx: &mut TxContext): NFT { + NFT { + id: object::new(ctx), + price: coin::into_balance(payment), + } + } + + /// Sell NFT + public fun sell_nft(nft: NFT, ctx: &mut TxContext): Coin { + let NFT {id, price} = nft; + object::delete(id); + coin::from_balance(price, ctx) + } +} \ No newline at end of file diff --git a/unit-five/lessons/2_hot_potato_pattern.md b/unit-five/lessons/2_hot_potato_pattern.md index c805604..a8fa6eb 100644 --- a/unit-five/lessons/2_hot_potato_pattern.md +++ b/unit-five/lessons/2_hot_potato_pattern.md @@ -71,3 +71,64 @@ public fun repay(pool: &mut LoanPool, loan: Loan, payment: Coin) { Users at some point must `repay()` the loan before the PTB ends. We consume the `Loan` by unpacking it, otherwise, you will receive compiler error if you use its fields with direct access `loan.amount` as `Loan` is non-`drop`. After unpacking, we simply use the loan amount to perform valid payment check and update the `LoanPool` accordingly. +## Example + +Let's try to create an example with flashloan where we borrow some SUI amount, use it to mint a dummy NFT and sell it to repay the debt. We will learn how to use PTB with Sui CLI to execute this all in one transaction. + +```rust +/// A dummy NFT to represent the flashloan functionality +struct NFT has key{ + id: UID, + price: Balance, +} + +/// Mint NFT + public fun mint_nft(payment: Coin, ctx: &mut TxContext): NFT { + NFT { + id: object::new(ctx), + price: coin::into_balance(payment), + } + } + +/// Sell NFT +public fun sell_nft(nft: NFT, ctx: &mut TxContext): Coin { + let NFT {id, price} = nft; + object::delete(id); + coin::from_balance(price, ctx) +} +``` + +You should able to publish the smart contract using the previous guide. After the smart deployment, we should have the package ID and the shared `LoanPool` object. Let's export them so we can use it later. + +```bash +export LOAN_PACKAGE_ID= +export LOAN_POOL_ID= +``` + +You need to deposit some SUI amount using `flashloan::deposit_pool` function. For demonstration purpose, we will deposit 10_000 MIST in the loan pool. + +```bash +sui client ptb \ +--split-coins gas "[10000]" \ +--assign coin \ +--move-call $LOAN_PACKAGE_ID::flashloan::deposit_pool @$LOAN_POOL_ID coin.0 \ +--gas-budget 10000000 +``` + +Now let's build a PTB that `borrow() -> mint_nft() -> sell_nft() -> repay()`. + +```bash +sui client ptb \ +--move-call $LOAN_PACKAGE_ID::flashloan::borrow @$LOAN_POOL_ID 10000 \ +--assign borrow_res \ +--move-call $LOAN_PACKAGE_ID::flashloan::mint_nft borrow_res.0 \ +--assign nft \ +--move-call $LOAN_PACKAGE_ID::flashloan::sell_nft nft \ +--assign repay_coin \ +--move-call $LOAN_PACKAGE_ID::flashloan::repay @$LOAN_POOL_ID borrow_res.1 repay_coin \ +--gas-budget 10000000 +``` + +*Quiz: What happen if you don't call `repay()` at the end of the PTB, please try it yourself* + +*💡Note: You may want to check out [SuiVision](https://testnet.suivision.xyz/) or [SuiScan](https://suiscan.xyz/testnet/home) to inspect the PTB for more details* \ No newline at end of file From 06e08d4cac5f16c0799905a85eda4fceefbffc79 Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Thu, 28 Mar 2024 17:28:36 +0700 Subject: [PATCH 12/16] feat: add create kiosk section --- unit-five/example_projects/kiosk/Move.lock | 2 +- .../example_projects/kiosk/sources/kiosk.move | 9 +++-- .../lessons/{3_kiosk.md => 3_kiosk_basics.md} | 21 ----------- unit-five/lessons/4_create_kiosk.md | 36 +++++++++++++++++++ 4 files changed, 43 insertions(+), 25 deletions(-) rename unit-five/lessons/{3_kiosk.md => 3_kiosk_basics.md} (93%) create mode 100644 unit-five/lessons/4_create_kiosk.md diff --git a/unit-five/example_projects/kiosk/Move.lock b/unit-five/example_projects/kiosk/Move.lock index cd31626..1c1853c 100644 --- a/unit-five/example_projects/kiosk/Move.lock +++ b/unit-five/example_projects/kiosk/Move.lock @@ -22,6 +22,6 @@ dependencies = [ ] [move.toolchain-version] -compiler-version = "1.20.0" +compiler-version = "1.21.0" edition = "legacy" flavor = "sui" diff --git a/unit-five/example_projects/kiosk/sources/kiosk.move b/unit-five/example_projects/kiosk/sources/kiosk.move index 1c2539a..0a5bfb6 100644 --- a/unit-five/example_projects/kiosk/sources/kiosk.move +++ b/unit-five/example_projects/kiosk/sources/kiosk.move @@ -5,7 +5,7 @@ module kiosk::kiosk { use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; use sui::tx_context::{TxContext, sender}; use sui::object::{Self, UID}; - use sui::coin::{Self, Coin}; + use sui::coin::{Coin}; use sui::sui::{SUI}; use sui::transfer_policy::{Self, TransferRequest, TransferPolicy, TransferPolicyCap}; use sui::package::{Self, Publisher}; @@ -28,9 +28,12 @@ module kiosk::kiosk { } } + #[allow(lint(share_owned, self_transfer))] /// Create new kiosk - public fun new_kiosk(ctx: &mut TxContext): (Kiosk, KioskOwnerCap) { - kiosk::new(ctx) + public fun new_kiosk(ctx: &mut TxContext) { + let (kiosk, kiosk_owner_cap) = kiosk::new(ctx); + transfer::public_share_object(kiosk); + transfer::public_transfer(kiosk_owner_cap, sender(ctx)); } /// Place item inside Kiosk diff --git a/unit-five/lessons/3_kiosk.md b/unit-five/lessons/3_kiosk_basics.md similarity index 93% rename from unit-five/lessons/3_kiosk.md rename to unit-five/lessons/3_kiosk_basics.md index 17023a1..5a09f7f 100644 --- a/unit-five/lessons/3_kiosk.md +++ b/unit-five/lessons/3_kiosk_basics.md @@ -55,27 +55,6 @@ _💡Note: there is another state called `LISTED EXCLUSIVELY`, which is not cove ## Sui Kiosk Usage -### Create Kiosk - -```rust -module kiosk::kiosk { - use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; - use sui::tx_context::{TxContext}; - - /// Create new kiosk - public fun new_kiosk(ctx: &mut TxContext): (Kiosk, KioskOwnerCap) { - kiosk::new(ctx) - } -} -``` - -There are 2 ways to create a new kiosk: - -- Use `kiosk::new()` to create new kiosk but we have to make the `Kiosk` shared object and transfer the `KioskOwnerCap` to the sender ourselves by using `sui::transfer` in the same PTB. -- Use `kiosk::default()` to automatically do all above steps for us. However, remeber that `kiosk::default()` is an entry function, so we can't include other calls in the same PTB. - -_💡Note: Kiosk is heterogenous collection by default so that's why it doesn't need type parameter for their items_ - ### Place Item inside Kiosk ```rust diff --git a/unit-five/lessons/4_create_kiosk.md b/unit-five/lessons/4_create_kiosk.md new file mode 100644 index 0000000..37b1adb --- /dev/null +++ b/unit-five/lessons/4_create_kiosk.md @@ -0,0 +1,36 @@ +# Create Kiosk + +Let's first deploy the example kiosk smart contract and export the package ID for later use. +```bash +export KIOSK_PACKAGE_ID= +``` + +```rust +module kiosk::kiosk { + use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; + use sui::tx_context::{TxContext}; + + #[allow(lint(share_owned, self_transfer))] + /// Create new kiosk + public fun new_kiosk(ctx: &mut TxContext) { + let (kiosk, kiosk_owner_cap) = kiosk::new(ctx); + transfer::public_share_object(kiosk); + transfer::public_transfer(kiosk_owner_cap, sender(ctx)); + } +} +``` + +There are 2 ways to create a new kiosk: +1. Use `kiosk::new()` to create new kiosk but we have to make the `Kiosk` shared object and transfer the `KioskOwnerCap` to the sender ourselves by using `sui::transfer`. +```bash +sui client call --package $KIOSK_PACKAGE_ID --module kiosk --function new_kiosk --gas-budget 10000000 +``` +2. Use `entry kiosk::default()` to automatically do all above steps for us. + +You can export the newly created `Kiosk` and its `KioskOwnerCap` for later use. +```bash +export KIOSK= +export KIOSK_OWNER_CAP= +``` + +_💡Note: Kiosk is heterogenous collection by default so that's why it doesn't need type parameter for their items_ From 4724b2bc7e50c4ff057b1f728b97cda07328452a Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Sun, 31 Mar 2024 11:23:06 +0700 Subject: [PATCH 13/16] feat: add kiosk buy full flow example --- .../kiosk/sources/fixed_royalty_rule.move | 12 +- .../example_projects/kiosk/sources/kiosk.move | 7 +- unit-five/lessons/3_kiosk_basics.md | 186 ----------------- ...create_kiosk.md => 4_kiosk_basic_usage.md} | 47 ++++- unit-five/lessons/5_transfer_policy.md | 193 ++++++++++++++++++ 5 files changed, 250 insertions(+), 195 deletions(-) rename unit-five/lessons/{4_create_kiosk.md => 4_kiosk_basic_usage.md} (53%) create mode 100644 unit-five/lessons/5_transfer_policy.md diff --git a/unit-five/example_projects/kiosk/sources/fixed_royalty_rule.move b/unit-five/example_projects/kiosk/sources/fixed_royalty_rule.move index 52b5a9e..536e6ea 100644 --- a/unit-five/example_projects/kiosk/sources/fixed_royalty_rule.move +++ b/unit-five/example_projects/kiosk/sources/fixed_royalty_rule.move @@ -7,7 +7,7 @@ module kiosk::fixed_royalty_rule { use sui::sui::SUI; use sui::coin::{Self, Coin}; use sui::transfer_policy::{ - Self as policy, + Self, TransferPolicy, TransferPolicyCap, TransferRequest @@ -43,7 +43,7 @@ module kiosk::fixed_royalty_rule { ) { assert!(amount_bp <= MAX_BPS, EIncorrectArgument); - policy::add_rule(Rule {}, policy, cap, Config { amount_bp, min_amount }) + transfer_policy::add_rule(Rule {}, policy, cap, Config { amount_bp, min_amount }) } /// Buyer action: Pay the royalty fee for the transfer. @@ -52,19 +52,19 @@ module kiosk::fixed_royalty_rule { request: &mut TransferRequest, payment: Coin ) { - let paid = policy::paid(request); + let paid = transfer_policy::paid(request); let amount = fee_amount(policy, paid); assert!(coin::value(&payment) == amount, EInsufficientAmount); - policy::add_to_balance(Rule {}, policy, payment); - policy::add_receipt(Rule {}, request) + transfer_policy::add_to_balance(Rule {}, policy, payment); + transfer_policy::add_receipt(Rule {}, request) } /// Helper function to calculate the amount to be paid for the transfer. /// Can be used dry-runned to estimate the fee amount based on the Kiosk listing price. public fun fee_amount(policy: &TransferPolicy, paid: u64): u64 { - let config: &Config = policy::get_rule(Rule {}, policy); + let config: &Config = transfer_policy::get_rule(Rule {}, policy); let amount = (((paid as u128) * (config.amount_bp as u128) / 10_000) as u64); // If the amount is less than the minimum, use the minimum diff --git a/unit-five/example_projects/kiosk/sources/kiosk.move b/unit-five/example_projects/kiosk/sources/kiosk.move index 0a5bfb6..cc9d76c 100644 --- a/unit-five/example_projects/kiosk/sources/kiosk.move +++ b/unit-five/example_projects/kiosk/sources/kiosk.move @@ -61,8 +61,11 @@ module kiosk::kiosk { transfer_policy::confirm_request(policy, req); } + #[allow(lint(share_owned, self_transfer))] /// Create new policy for type `T` - public fun new_policy(publisher: &Publisher, ctx: &mut TxContext): (TransferPolicy, TransferPolicyCap) { - transfer_policy::new(publisher, ctx) + public fun new_policy(publisher: &Publisher, ctx: &mut TxContext) { + let (policy, policy_cap) = transfer_policy::new(publisher, ctx); + transfer::public_share_object(policy); + transfer::public_transfer(policy_cap, sender(ctx)); } } \ No newline at end of file diff --git a/unit-five/lessons/3_kiosk_basics.md b/unit-five/lessons/3_kiosk_basics.md index 5a09f7f..d6bfeca 100644 --- a/unit-five/lessons/3_kiosk_basics.md +++ b/unit-five/lessons/3_kiosk_basics.md @@ -52,189 +52,3 @@ When you add an asset to your kiosk, it has one of the following states: - `LISTED` - an item in the kiosk that is listed for sale. The Kiosk Owner can’t modify an item while listed, but you can borrow it immutably or delist it, which returns it to its previous state. _💡Note: there is another state called `LISTED EXCLUSIVELY`, which is not covered in this unit and will be covered in the future in advanced section_ - -## Sui Kiosk Usage - -### Place Item inside Kiosk - -```rust -struct TShirt has key, store { - id: UID, -} - -public fun new_tshirt(ctx: &mut TxContext): TShirt { - TShirt { - id: object::new(ctx), - } -} - -/// Place item inside kiosk -public fun place(kiosk: &mut Kiosk, cap: &KioskOwnerCap, item: TShirt) { - kiosk::place(kiosk, cap, item) -} -``` - -We can use `kiosk::place()` API to place an item inside kiosk. Remember that only the Kiosk Owner can have access to this API. - -### Withdraw Item from Kiosk - -```rust -/// Withdraw item from Kiosk -public fun withdraw(kiosk: &mut Kiosk, cap: &KioskOwnerCap, item_id: object::ID): TShirt { - kiosk::take(kiosk, cap, item_id) -} -``` - -We can use `kiosk::take()` API to withdraw an item from kiosk. Remember that only the Kiosk Owner can have access to this API. - -### List Item for Sale - -```rust -/// List item for sale -public fun list(kiosk: &mut Kiosk, cap: &KioskOwnerCap, item_id: object::ID, price: u64) { - kiosk::list(kiosk, cap, item_id, price) -} -``` - -We can use `kiosk::list()` API to list an item for sale. Remember that only the Kiosk Owner can have access to this API. - -### Buy Item from Kiosk - -```rust -use sui::transfer_policy::{Self, TransferRequest, TransferPolicy}; - -/// Buy listed item -public fun buy(kiosk: &mut Kiosk, item_id: object::ID, payment: Coin): (TShirt, TransferRequest){ - kiosk::purchase(kiosk, item_id, payment) -} - -/// Confirm the TransferRequest -public fun confirm_request(policy: &TransferPolicy, req: TransferRequest) { - transfer_policy::confirm_request(policy, req); -} -``` - -When buyers buy the asset by using `kiosk::purchase()` API, an item is returned alongside with a `TransferRequest`. `TransferRequest` is a hot potato forcing us to consume it through `transfer_policy::confirm_request()`. `confirm_request()`'s job is to verify whether all the rules configured and enabled in the `TransferPolicy` are complied by the users. If one of the enabled rules are not satisfied, then `confirm_request()` throws error leading to the failure of the transaction. As a consequence, the item is not under your ownership even if you tried to transfer the item to your account before `confirm_request()`. - -_💡Note: The users must compose a PTB with all necessary calls to ensure the TransferRequest is valid before `confirm_request()` call._ - -The flow can be illustrated as follow: - -_Buyer -> `kiosk::purchase()` -> `Item` + `TransferRequest` -> Subsequent calls to fulfill `TransferRequest` -> `transfer_policy::confirm_request()` -> Transfer `Item` under ownership_ - -### Create a `TransferPolicy` - -`TransferPolicy` for type `T` must be created for that type `T` to be tradeable in the Kiosk system. `TransferPolicy` is a shared object acting as a central authority so that everyone can use it to `confirm_request()`. - -```rust -use sui::tx_context::{TxContext, sender}; -use sui::transfer_policy::{Self, TransferRequest, TransferPolicy, TransferPolicyCap}; -use sui::package::{Self, Publisher}; -use sui::transfer::{Self}; - -struct KIOSK has drop {} - -fun init(witness: KIOSK, ctx: &mut TxContext) { - let publisher = package::claim(otw, ctx); - transfer::public_transfer(publisher, sender(ctx)); -} - -/// Create new policy for type `T` -public fun new_policy(publisher: &Publisher, ctx: &mut TxContext): (TransferPolicy, TransferPolicyCap) { - transfer_policy::new(publisher, ctx) -} -``` - -Create a `TransferPolicy` requires the proof of publisher `Publisher` of the module comprising `T`. This ensures only the creator of type `T` can create `TransferPolicy`. There are 2 ways to create the policy: - -- Use `transfer_policy::new()` to create new policy but we have to make the `TransferPolicy` shared object and transfer the `TransferPolicyCap` to the `Publisher` ourselves by using `sui::transfer` in the same PTB. -- Use `transfer_policy::default()` to automatically do all above steps for us. However, remeber that `transfer_policy::default()` is an entry function, so we can't include other calls in the same PTB. - -### Implement Fixed Fee Rule - -`TransferPolicy` doesn't enforce anything without any rule, let's learn how to implement a simple rule in a separated module to enforce users to pay a fixed royalty fee for a trade to succeed. - -*💡Note: There is a standard approach to implement the rules. Please checkout the [rule template here](../example_projects/kiosk/sources/dummy_policy.move)* - -#### Rule Witness & Rule Config -```rust -module kiosk::fixed_royalty_rule { - /// The `amount_bp` passed is more than 100%. - const EIncorrectArgument: u64 = 0; - /// The `Coin` used for payment is not enough to cover the fee. - const EInsufficientAmount: u64 = 1; - - /// Max value for the `amount_bp`. - const MAX_BPS: u16 = 10_000; - - /// The Rule Witness to authorize the policy - struct Rule has drop {} - - /// Configuration for the Rule - struct Config has store, drop { - /// Percentage of the transfer amount to be paid as royalty fee - amount_bp: u16, - /// This is used as royalty fee if the calculated fee is smaller than `min_amount` - min_amount: u64, - } -} -``` - -`Rule` represents a witness type to add to `TransferPolicy`, it helps to identify and distinguish between multiple rules adding to one policy. `Config` is the configuration of the `Rule`, as we implement fixed royaltee fee, the settings should include the percentage we want to deduct out of orignal payment. - -#### Add Rule to TransferPolicy - -```rust -/// Function that adds a Rule to the `TransferPolicy`. -/// Requires `TransferPolicyCap` to make sure the rules are -/// added only by the publisher of T. -public fun add( - policy: &mut TransferPolicy, - cap: &TransferPolicyCap, - amount_bp: u16, - min_amount: u64 - -) { - assert!(amount_bp <= MAX_BPS, EIncorrectArgument); - policy::add_rule(Rule {}, policy, cap, Config { amount_bp, min_amount }) -} -``` - -We use `policy::add_rule()` to add the rule with its configuration to the policy. - -#### Buyers Follow the Rule - -```rust -/// Buyer action: Pay the royalty fee for the transfer. -public fun pay( - policy: &mut TransferPolicy, - request: &mut TransferRequest, - payment: Coin -) { - let paid = policy::paid(request); - let amount = fee_amount(policy, paid); - - assert!(coin::value(&payment) == amount, EInsufficientAmount); - - policy::add_to_balance(Rule {}, policy, payment); - policy::add_receipt(Rule {}, request) -} - -/// Helper function to calculate the amount to be paid for the transfer. -/// Can be used dry-runned to estimate the fee amount based on the Kiosk listing price. -public fun fee_amount(policy: &TransferPolicy, paid: u64): u64 { - let config: &Config = policy::get_rule(Rule {}, policy); - let amount = (((paid as u128) * (config.amount_bp as u128) / 10_000) as u64); - - // If the amount is less than the minimum, use the minimum - if (amount < config.min_amount) { - amount = config.min_amount - }; - - amount -} -``` - -We need a helper `fee_amount()` to calculate the royalty fee given the policy and the payment amount. We use `policy::get_rule()` to enquire the configuration and use it for fee calculation. - -`pay()` is a function that users must call themselves to fullfil the `TransferRequest` before `confirm_request()`. `policy::paid()` gives us original payment of the trade embedded in the `TransferRequest`. After royalty fee calculation, we will add the fee to the policy through `policy::add_to_balance()`, any fee collected by the policy is accumulated here and `TransferPolicyCap` owner can withdraw later. Last but not least, we use `policy::add_receipt()` to flag the `TransferRequest` that this rule is passed and ready to be confirmed with `confirm_request()`. \ No newline at end of file diff --git a/unit-five/lessons/4_create_kiosk.md b/unit-five/lessons/4_kiosk_basic_usage.md similarity index 53% rename from unit-five/lessons/4_create_kiosk.md rename to unit-five/lessons/4_kiosk_basic_usage.md index 37b1adb..131197a 100644 --- a/unit-five/lessons/4_create_kiosk.md +++ b/unit-five/lessons/4_kiosk_basic_usage.md @@ -1,4 +1,6 @@ -# Create Kiosk +# Kiosk Basic Usage + +## Create Kiosk Let's first deploy the example kiosk smart contract and export the package ID for later use. ```bash @@ -34,3 +36,46 @@ export KIOSK_OWNER_CAP= ``` _💡Note: Kiosk is heterogenous collection by default so that's why it doesn't need type parameter for their items_ + +## Place Item inside Kiosk + +```rust +struct TShirt has key, store { + id: UID, +} + +public fun new_tshirt(ctx: &mut TxContext): TShirt { + TShirt { + id: object::new(ctx), + } +} + +/// Place item inside kiosk +public fun place(kiosk: &mut Kiosk, cap: &KioskOwnerCap, item: TShirt) { + kiosk::place(kiosk, cap, item) +} +``` + +We can use `kiosk::place()` API to place an item inside kiosk. Remember that only the Kiosk Owner can have access to this API. + +## Withdraw Item from Kiosk + +```rust +/// Withdraw item from Kiosk +public fun withdraw(kiosk: &mut Kiosk, cap: &KioskOwnerCap, item_id: object::ID): TShirt { + kiosk::take(kiosk, cap, item_id) +} +``` + +We can use `kiosk::take()` API to withdraw an item from kiosk. Remember that only the Kiosk Owner can have access to this API. + +## List Item for Sale + +```rust +/// List item for sale +public fun list(kiosk: &mut Kiosk, cap: &KioskOwnerCap, item_id: object::ID, price: u64) { + kiosk::list(kiosk, cap, item_id, price) +} +``` + +We can use `kiosk::list()` API to list an item for sale. Remember that only the Kiosk Owner can have access to this API. diff --git a/unit-five/lessons/5_transfer_policy.md b/unit-five/lessons/5_transfer_policy.md new file mode 100644 index 0000000..2c56faf --- /dev/null +++ b/unit-five/lessons/5_transfer_policy.md @@ -0,0 +1,193 @@ +# Transfer Policy and Buy from Kiosk + +In this section, we will learn how to create a `TransferPolicy` and use it to enforce rules the buyers must comply before the purchased item is owned by them. + +## `TransferPolicy` +### Create a `TransferPolicy` + +`TransferPolicy` for type `T` must be created for that type `T` to be tradeable in the Kiosk system. `TransferPolicy` is a shared object acting as a central authority enforcing everyone to check their purchase is valid against the defined policy before the purchased item is transferred to the buyers. + +```rust +use sui::tx_context::{TxContext, sender}; +use sui::transfer_policy::{Self, TransferRequest, TransferPolicy, TransferPolicyCap}; +use sui::package::{Self, Publisher}; +use sui::transfer::{Self}; + +struct KIOSK has drop {} + +fun init(witness: KIOSK, ctx: &mut TxContext) { + let publisher = package::claim(otw, ctx); + transfer::public_transfer(publisher, sender(ctx)); +} + +#[allow(lint(share_owned, self_transfer))] +/// Create new policy for type `T` +public fun new_policy(publisher: &Publisher, ctx: &mut TxContext) { + let (policy, policy_cap) = transfer_policy::new(publisher, ctx); + transfer::public_share_object(policy); + transfer::public_transfer(policy_cap, sender(ctx)); +} +``` + +Create a `TransferPolicy` requires the proof of publisher `Publisher` of the module comprising `T`. This ensures only the creator of type `T` can create `TransferPolicy`. There are 2 ways to create the policy: + +- Use `transfer_policy::new()` to create new policy, make the `TransferPolicy` shared object and transfer the `TransferPolicyCap` to the sender by using `sui::transfer`. +```bash +sui client call --package $KIOSK_PACKAGE_ID --module kiosk --function new_policy --args $KIOSK_PUBLISHER --gas-budget 10000000 +``` +- Use `entry transfer_policy::default()` to automatically do all above steps for us. + +You should already receive the `Publisher` object when publish the package. Let's export it for later use. +```bash +export KIOSK_PUBLISHER= +``` + +You should see the newly created `TransferPolicy` object and `TransferPolicyCap` object in the terminal. Let's export it for later use. +```bash +export KIOSK_TRANSFER_POLICY= +export KIOSK_TRANSFER_POLICY_CAP= +``` + +### Implement Fixed Fee Rule + +`TransferPolicy` doesn't enforce anything without any rule, let's learn how to implement a simple rule in a separated module to enforce users to pay a fixed royalty fee for a trade to succeed. + +_💡Note: There is a standard approach to implement the rules. Please checkout the [rule template here](../example_projects/kiosk/sources/dummy_policy.move)_ + +#### Rule Witness & Rule Config + +```rust +module kiosk::fixed_royalty_rule { + /// The `amount_bp` passed is more than 100%. + const EIncorrectArgument: u64 = 0; + /// The `Coin` used for payment is not enough to cover the fee. + const EInsufficientAmount: u64 = 1; + + /// Max value for the `amount_bp`. + const MAX_BPS: u16 = 10_000; + + /// The Rule Witness to authorize the policy + struct Rule has drop {} + + /// Configuration for the Rule + struct Config has store, drop { + /// Percentage of the transfer amount to be paid as royalty fee + amount_bp: u16, + /// This is used as royalty fee if the calculated fee is smaller than `min_amount` + min_amount: u64, + } +} +``` + +`Rule` represents a witness type to add to `TransferPolicy`, it helps to identify and distinguish between multiple rules adding to one policy. `Config` is the configuration of the `Rule`, as we implement fixed royaltee fee, the settings should include the percentage we want to deduct out of orignal payment. + +#### Add Rule to TransferPolicy + +```rust +/// Function that adds a Rule to the `TransferPolicy`. +/// Requires `TransferPolicyCap` to make sure the rules are +/// added only by the publisher of T. +public fun add( + policy: &mut TransferPolicy, + cap: &TransferPolicyCap, + amount_bp: u16, + min_amount: u64 + +) { + assert!(amount_bp <= MAX_BPS, EIncorrectArgument); + transfer_policy::add_rule(Rule {}, policy, cap, Config { amount_bp, min_amount }) +} +``` + +We use `transfer_policy::add_rule()` to add the rule with its configuration to the policy. + +Let's execute this function from the client to add the `Rule` to the `TransferPolicy`, otherwise, it is disabled. In this example, we configure the percentage of royalty fee is `0.1%` ~ `10 basis points` and the minimum amount royalty fee is `100 MIST`. +```bash +sui client call --package $KIOSK_PACKAGE_ID --module fixed_royalty_rule --function add --args $KIOSK_TRANSFER_POLICY $KIOSK_TRANSFER_POLICY_CAP 10 100 --type-args $KIOSK_PACKAGE_ID::kiosk::TShirt --gas-budget 10000000 +``` + +#### Satisfy the Rule + +```rust +/// Buyer action: Pay the royalty fee for the transfer. +public fun pay( + policy: &mut TransferPolicy, + request: &mut TransferRequest, + payment: Coin +) { + let paid = transfer_policy::paid(request); + let amount = fee_amount(policy, paid); + + assert!(coin::value(&payment) == amount, EInsufficientAmount); + + transfer_policy::add_to_balance(Rule {}, policy, payment); + transfer_policy::add_receipt(Rule {}, request) +} + +/// Helper function to calculate the amount to be paid for the transfer. +/// Can be used dry-runned to estimate the fee amount based on the Kiosk listing price. +public fun fee_amount(policy: &TransferPolicy, paid: u64): u64 { + let config: &Config = transfer_policy::get_rule(Rule {}, policy); + let amount = (((paid as u128) * (config.amount_bp as u128) / 10_000) as u64); + + // If the amount is less than the minimum, use the minimum + if (amount < config.min_amount) { + amount = config.min_amount + }; + + amount +} +``` + +We need a helper `fee_amount()` to calculate the royalty fee given the policy and the payment amount. We use `transfer_policy::get_rule()` to enquire the configuration and use it for fee calculation. + +`pay()` is a function that users must call themselves to fullfil the `TransferRequest` (described in the next section) before `transfer_policy::confirm_request()`. `transfer_policy::paid()` gives us original payment of the trade represented by `TransferRequest`. After royalty fee calculation, we will add the fee to the policy through `transfer_policy::add_to_balance()`, any fee collected by the policy is accumulated here and `TransferPolicyCap` owner can withdraw later. Last but not least, we use `transfer_policy::add_receipt()` to flag the `TransferRequest` that this rule is passed and ready to be confirmed with `transfer_policy::confirm_request()`. + +## Buy Item from Kiosk + +```rust +use sui::transfer_policy::{Self, TransferRequest, TransferPolicy}; + +/// Buy listed item +public fun buy(kiosk: &mut Kiosk, item_id: object::ID, payment: Coin): (TShirt, TransferRequest){ + kiosk::purchase(kiosk, item_id, payment) +} + +/// Confirm the TransferRequest +public fun confirm_request(policy: &TransferPolicy, req: TransferRequest) { + transfer_policy::confirm_request(policy, req); +} +``` + +When buyers buy the asset by using `kiosk::purchase()` API, an item is returned alongside with a `TransferRequest`. `TransferRequest` is a hot potato forcing us to consume it through `transfer_policy::confirm_request()`. `transfer_policy::confirm_request()`'s job is to verify whether all the rules configured and enabled in the `TransferPolicy` are complied by the users. If one of the enabled rules are not satisfied, then `transfer_policy::confirm_request()` throws error leading to the failure of the transaction. As a consequence, the item is not under your ownership even if you tried to transfer the item to your account before `transfer_policy::confirm_request()`. + +_💡Note: The users must compose a PTB with all necessary calls to ensure the TransferRequest is valid before `confirm_request()` call._ + +The flow can be illustrated as follow: + +_Buyer -> `kiosk::purchase()` -> `Item` + `TransferRequest` -> Subsequent calls to fulfill `TransferRequest` -> `transfer_policy::confirm_request()` -> Transfer `Item` under ownership_ + +## Kiosk Full Flow Example + +Recall from the previous section, the item must be placed inside the kiosk, then it must be listed to become sellable. Assuming the item is already listed with price `10_000 MIST`, let's export the listed item as terminal variable. +```bash +export KIOSK_TSHIRT= +``` + +Let's build a PTB to execute a trade. The flow is straightforward, we buy the listed item from the kiosk, the item and `TransferRequest` is returned, then, we call `fixed_royalty_fee::pay` to fullfil the `TransferRequest`, we confirm the `TransferRequest` with `confirm_request()` before finally transfer the item to the buyer. +```bash +sui client ptb \ +--assign price 10000 \ +--split-coins gas "[price]" \ +--assign coin \ +--move-call $KIOSK_PACKAGE_ID::kiosk::buy @$KIOSK @$KIOSK_TSHIRT coin.0 \ +--assign buy_res \ +--move-call $KIOSK_PACKAGE_ID::fixed_royalty_rule::fee_amount "<$KIOSK_PACKAGE_ID::kiosk::TShirt>" @$KIOSK_TRANSFER_POLICY price \ +--assign fee_amount \ +--split-coins gas "[fee_amount]"\ +--assign coin \ +--move-call $KIOSK_PACKAGE_ID::fixed_royalty_rule::pay "<$KIOSK_PACKAGE_ID::kiosk::TShirt>" @$KIOSK_TRANSFER_POLICY buy_res.1 coin.0 \ +--move-call $KIOSK_PACKAGE_ID::kiosk::confirm_request @$KIOSK_TRANSFER_POLICY buy_res.1 \ +--move-call 0x2::transfer::public_transfer "<$KIOSK_PACKAGE_ID::kiosk::TShirt>" buy_res.0 tester \ +--gas-budget 10000000 +``` From 12fbf028820ad0aa4287b101e65b89a5fa5b723f Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Sun, 31 Mar 2024 11:24:18 +0700 Subject: [PATCH 14/16] feat: update readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 23fa566..5340941 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,9 @@ Introductory course to the Move language maintained by [Sui Foundation](https:// - **Unit Five: Sui Kiosk** - [Programmable Transaction Block](./unit-five/lessons/1_programmable_transaction_block.md) - [Hot Potato Design Pattern](./unit-five/lessons/2_hot_potato_pattern.md) - - [Sui Kiosk](./unit-five/lessons/3_kiosk.md) + - [Sui Kiosk Basic Concepts](./unit-five/lessons/3_kiosk_basics.md) + - [Sui Kiosk Basic Usage](./unit-five/lessons/4_kiosk_basic_usage.md) + - [Transfer Policy](./unit-five/lessons/5_transfer_policy.md) - **Advanced Topics** - [BCS Encoding](./advanced-topics/BCS_encoding/lessons/BCS_encoding.md) From 5801092914e2f402a4a86254d1ef47e1ad40caa0 Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Sun, 31 Mar 2024 11:45:18 +0700 Subject: [PATCH 15/16] fix: fix ptb kiosk buy --- unit-five/lessons/5_transfer_policy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unit-five/lessons/5_transfer_policy.md b/unit-five/lessons/5_transfer_policy.md index 2c56faf..4229a74 100644 --- a/unit-five/lessons/5_transfer_policy.md +++ b/unit-five/lessons/5_transfer_policy.md @@ -188,6 +188,6 @@ sui client ptb \ --assign coin \ --move-call $KIOSK_PACKAGE_ID::fixed_royalty_rule::pay "<$KIOSK_PACKAGE_ID::kiosk::TShirt>" @$KIOSK_TRANSFER_POLICY buy_res.1 coin.0 \ --move-call $KIOSK_PACKAGE_ID::kiosk::confirm_request @$KIOSK_TRANSFER_POLICY buy_res.1 \ ---move-call 0x2::transfer::public_transfer "<$KIOSK_PACKAGE_ID::kiosk::TShirt>" buy_res.0 tester \ +--move-call 0x2::transfer::public_transfer "<$KIOSK_PACKAGE_ID::kiosk::TShirt>" buy_res.0 \ --gas-budget 10000000 ``` From 5aca07f68683f7bc8e136a1ce62738b59e96550f Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Mon, 8 Apr 2024 17:02:54 +0700 Subject: [PATCH 16/16] fix: change `rust` to `move` for code example --- unit-five/lessons/2_hot_potato_pattern.md | 8 ++++---- unit-five/lessons/4_kiosk_basic_usage.md | 8 ++++---- unit-five/lessons/5_transfer_policy.md | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/unit-five/lessons/2_hot_potato_pattern.md b/unit-five/lessons/2_hot_potato_pattern.md index a8fa6eb..e0173ff 100644 --- a/unit-five/lessons/2_hot_potato_pattern.md +++ b/unit-five/lessons/2_hot_potato_pattern.md @@ -4,7 +4,7 @@ A hot potato is a struct that has no capabilities, therefore you can only pack a ## Type Definitions -```rust +```move module flashloan::flashloan { // === Imports === use sui::sui::SUI; @@ -38,7 +38,7 @@ We have a `LoanPool` shared object acting as a money vault ready for users to bo ## Borrow -```rust +```move /// Function allows users to borrow from the loan pool. /// It returns the borrowed [`Coin`] and the [`Loan`] position /// enforcing users to fulfill before the PTB ends. @@ -58,7 +58,7 @@ Users can borrow the money from the `LoanPool` by calling `borrow()`. Basically, ## Repay -```rust +```move /// Repay the loan /// Users must execute this function to ensure the loan is repaid before the transaction ends. public fun repay(pool: &mut LoanPool, loan: Loan, payment: Coin) { @@ -75,7 +75,7 @@ Users at some point must `repay()` the loan before the PTB ends. We consume the Let's try to create an example with flashloan where we borrow some SUI amount, use it to mint a dummy NFT and sell it to repay the debt. We will learn how to use PTB with Sui CLI to execute this all in one transaction. -```rust +```move /// A dummy NFT to represent the flashloan functionality struct NFT has key{ id: UID, diff --git a/unit-five/lessons/4_kiosk_basic_usage.md b/unit-five/lessons/4_kiosk_basic_usage.md index 131197a..2e40e02 100644 --- a/unit-five/lessons/4_kiosk_basic_usage.md +++ b/unit-five/lessons/4_kiosk_basic_usage.md @@ -7,7 +7,7 @@ Let's first deploy the example kiosk smart contract and export the package ID fo export KIOSK_PACKAGE_ID= ``` -```rust +```move module kiosk::kiosk { use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; use sui::tx_context::{TxContext}; @@ -39,7 +39,7 @@ _💡Note: Kiosk is heterogenous collection by default so that's why it doesn't ## Place Item inside Kiosk -```rust +```move struct TShirt has key, store { id: UID, } @@ -60,7 +60,7 @@ We can use `kiosk::place()` API to place an item inside kiosk. Remember that onl ## Withdraw Item from Kiosk -```rust +```move /// Withdraw item from Kiosk public fun withdraw(kiosk: &mut Kiosk, cap: &KioskOwnerCap, item_id: object::ID): TShirt { kiosk::take(kiosk, cap, item_id) @@ -71,7 +71,7 @@ We can use `kiosk::take()` API to withdraw an item from kiosk. Remember that onl ## List Item for Sale -```rust +```move /// List item for sale public fun list(kiosk: &mut Kiosk, cap: &KioskOwnerCap, item_id: object::ID, price: u64) { kiosk::list(kiosk, cap, item_id, price) diff --git a/unit-five/lessons/5_transfer_policy.md b/unit-five/lessons/5_transfer_policy.md index 4229a74..9733cfa 100644 --- a/unit-five/lessons/5_transfer_policy.md +++ b/unit-five/lessons/5_transfer_policy.md @@ -7,7 +7,7 @@ In this section, we will learn how to create a `TransferPolicy` and use it to en `TransferPolicy` for type `T` must be created for that type `T` to be tradeable in the Kiosk system. `TransferPolicy` is a shared object acting as a central authority enforcing everyone to check their purchase is valid against the defined policy before the purchased item is transferred to the buyers. -```rust +```move use sui::tx_context::{TxContext, sender}; use sui::transfer_policy::{Self, TransferRequest, TransferPolicy, TransferPolicyCap}; use sui::package::{Self, Publisher}; @@ -56,7 +56,7 @@ _💡Note: There is a standard approach to implement the rules. Please checkout #### Rule Witness & Rule Config -```rust +```move module kiosk::fixed_royalty_rule { /// The `amount_bp` passed is more than 100%. const EIncorrectArgument: u64 = 0; @@ -83,7 +83,7 @@ module kiosk::fixed_royalty_rule { #### Add Rule to TransferPolicy -```rust +```move /// Function that adds a Rule to the `TransferPolicy`. /// Requires `TransferPolicyCap` to make sure the rules are /// added only by the publisher of T. @@ -108,7 +108,7 @@ sui client call --package $KIOSK_PACKAGE_ID --module fixed_royalty_rule --functi #### Satisfy the Rule -```rust +```move /// Buyer action: Pay the royalty fee for the transfer. public fun pay( policy: &mut TransferPolicy, @@ -145,7 +145,7 @@ We need a helper `fee_amount()` to calculate the royalty fee given the policy an ## Buy Item from Kiosk -```rust +```move use sui::transfer_policy::{Self, TransferRequest, TransferPolicy}; /// Buy listed item