diff --git a/.gitignore b/.gitignore index 5cf6bb1..a37c642 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ 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/ +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/README.md b/README.md index bb7985e..5340941 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,12 @@ 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_pattern.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) diff --git a/unit-five/example_projects/flashloan/Move.lock b/unit-five/example_projects/flashloan/Move.lock new file mode 100644 index 0000000..da55037 --- /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.21.0" +edition = "legacy" +flavor = "sui" diff --git a/unit-five/example_projects/flashloan/Move.toml b/unit-five/example_projects/flashloan/Move.toml new file mode 100644 index 0000000..524ad2a --- /dev/null +++ b/unit-five/example_projects/flashloan/Move.toml @@ -0,0 +1,40 @@ +[package] +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" +# 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] +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. +# 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/flashloan/sources/flashloan.move b/unit-five/example_projects/flashloan/sources/flashloan.move new file mode 100644 index 0000000..f748d1a --- /dev/null +++ b/unit-five/example_projects/flashloan/sources/flashloan.move @@ -0,0 +1,93 @@ +// 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::{Self, UID}; + use sui::tx_context::{TxContext}; + use sui::transfer::{Self}; + + // === 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, + } + + /// 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. + 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)); + } + + /// 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/example_projects/kiosk/Move.lock b/unit-five/example_projects/kiosk/Move.lock new file mode 100644 index 0000000..1c1853c --- /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.21.0" +edition = "legacy" +flavor = "sui" 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/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..536e6ea --- /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, + 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); + transfer_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 = 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 + } +} \ 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 new file mode 100644 index 0000000..cc9d76c --- /dev/null +++ b/unit-five/example_projects/kiosk/sources/kiosk.move @@ -0,0 +1,71 @@ +// Copyright (c) Sui Foundation, Inc. +// 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::{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), + } + } + + #[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)); + } + + /// 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); + } + + #[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)); + } +} \ No newline at end of file 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..730f0b4 --- /dev/null +++ b/unit-five/lessons/1_programmable_transaction_block.md @@ -0,0 +1,28 @@ +# 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. +- 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* + +## 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/lessons/2_hot_potato_pattern.md b/unit-five/lessons/2_hot_potato_pattern.md new file mode 100644 index 0000000..e0173ff --- /dev/null +++ b/unit-five/lessons/2_hot_potato_pattern.md @@ -0,0 +1,134 @@ +# 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 + +```move +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 + +```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. +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 + +```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) { + 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. + +## 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. + +```move +/// 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 diff --git a/unit-five/lessons/3_kiosk_basics.md b/unit-five/lessons/3_kiosk_basics.md new file mode 100644 index 0000000..d6bfeca --- /dev/null +++ b/unit-five/lessons/3_kiosk_basics.md @@ -0,0 +1,54 @@ +# 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_ 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. +- 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. + +## 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. + +_💡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_ diff --git a/unit-five/lessons/4_kiosk_basic_usage.md b/unit-five/lessons/4_kiosk_basic_usage.md new file mode 100644 index 0000000..2e40e02 --- /dev/null +++ b/unit-five/lessons/4_kiosk_basic_usage.md @@ -0,0 +1,81 @@ +# Kiosk Basic Usage + +## Create Kiosk + +Let's first deploy the example kiosk smart contract and export the package ID for later use. +```bash +export KIOSK_PACKAGE_ID= +``` + +```move +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_ + +## Place Item inside Kiosk + +```move +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 + +```move +/// 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 + +```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) +} +``` + +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..9733cfa --- /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. + +```move +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 + +```move +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 + +```move +/// 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 + +```move +/// 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 + +```move +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 \ +--gas-budget 10000000 +``` diff --git a/unit-five/readme.md b/unit-five/readme.md new file mode 100644 index 0000000..e69de29