From 1b0a05893d53ad818fbfd80dc6626c28eb9ae7ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=81abor?= Date: Fri, 14 Jul 2023 16:03:05 +0200 Subject: [PATCH 1/9] Initial --- contracts/kiosk/sources/kiosk/ob_kiosk.move | 169 ++++++++++-- .../sources/trading/orderbook.move | 241 ++++++++++++------ 2 files changed, 308 insertions(+), 102 deletions(-) diff --git a/contracts/kiosk/sources/kiosk/ob_kiosk.move b/contracts/kiosk/sources/kiosk/ob_kiosk.move index 8e4e3e83..02d7a6fa 100644 --- a/contracts/kiosk/sources/kiosk/ob_kiosk.move +++ b/contracts/kiosk/sources/kiosk/ob_kiosk.move @@ -39,7 +39,7 @@ module ob_kiosk::ob_kiosk { use sui::dynamic_field::{Self as df}; use sui::kiosk::{Self, Kiosk, KioskOwnerCap, uid, uid_mut as ext}; use sui::object::{Self, ID, UID, uid_to_address}; - use sui::coin; + use sui::coin::{Self, Coin}; use sui::table::{Self, Table}; use sui::transfer::{transfer, public_share_object, public_transfer}; use sui::tx_context::{Self, TxContext, sender}; @@ -55,6 +55,7 @@ module ob_kiosk::ob_kiosk { // Track the current version of the module const VERSION: u64 = 3; + const EDeprecatedApi: u64 = 998; const ENotUpgraded: u64 = 999; const EWrongVersion: u64 = 1000; @@ -328,8 +329,11 @@ module ob_kiosk::ob_kiosk { assert_version_and_upgrade(ext(self)); assert_can_deposit(self, ctx); + let nft_id = object::id(&nft); + let cap = pop_cap(self); - deposit_(self, &cap, nft); + kiosk::place(self, &cap, nft); + register_nft_(self, nft_id); set_cap(self, cap); } @@ -353,29 +357,41 @@ module ob_kiosk::ob_kiosk { let cap = pop_cap(self); while (!vector::is_empty(&nfts)) { let nft = vector::pop_back(&mut nfts); - deposit_(self, &cap, nft); + let nft_id = object::id(&nft); + kiosk::place(self, &cap, nft); + register_nft_(self, nft_id); }; vector::destroy_empty(nfts); set_cap(self, cap); } - /// Deposits NFT into `Kiosk` and handles `NftRef` accounting - fun deposit_( + /// Deposit an NFT and lock it within the `Kiosk` + /// + /// NFTs deposited using `deposit_locked` must use `transfer_locked_nft` to + /// transfer the NFT. + /// + /// Useful for interacting with non-OB collections. + /// + /// #### Panics + /// + /// Panics if transaction sender is not owner or `Kiosk` is not + /// permissionless. + public fun deposit_locked( self: &mut Kiosk, - cap: &KioskOwnerCap, + policy: &sui::transfer_policy::TransferPolicy, nft: T, + ctx: &mut TxContext, ) { - let nft_id = object::id(&nft); + assert_version_and_upgrade(ext(self)); + assert_can_deposit(self, ctx); - let refs = nft_refs_mut(self); - table::add(refs, nft_id, NftRef { - auths: vec_set::empty(), - is_exclusively_listed: false, - }); + let nft_id = object::id(&nft); - // place underlying NFT to kiosk - kiosk::place(self, cap, nft); + let cap = pop_cap(self); + kiosk::lock(self, &cap, policy, nft); + register_nft_(self, nft_id); + set_cap(self, cap); } // === Withdraw from the Kiosk === @@ -522,6 +538,8 @@ module ob_kiosk::ob_kiosk { /// Transfer NFT out of Kiosk that has been previously delegated /// + /// NFT will not be locked in the target `Kiosk`. + /// /// Requires that address of sender was previously passed to /// `auth_transfer`. /// @@ -546,6 +564,35 @@ module ob_kiosk::ob_kiosk { req } + /// Transfer NFT out of Kiosk that has been previously delegated + /// + /// NFT will be locked in the target `Kiosk`. + /// + /// Requires that address of sender was previously passed to + /// `auth_transfer`. + /// + /// #### Panics + /// + /// - Entity `UID` was not previously authorized for transfer + /// - NFT does not exist + /// - Target `Kiosk` deposit conditions were not met, see `deposit` method + /// - Source or target `Kiosk` are not OriginByte kiosks + public fun transfer_delegated_locked( + source: &mut Kiosk, + target: &mut Kiosk, + nft_id: ID, + entity_id: &UID, + price: u64, + transfer_policy: &sui::transfer_policy::TransferPolicy, + ctx: &mut TxContext, + ): TransferRequest { + assert_version_and_upgrade(ext(source)); + + let (nft, req) = transfer_nft_(source, nft_id, uid_to_address(entity_id), price, ctx); + deposit_locked(target, transfer_policy, nft, ctx); + req + } + /// Transfer NFT out of Kiosk that has been previously delegated /// /// Requires that address of sender was previously passed to @@ -576,8 +623,10 @@ module ob_kiosk::ob_kiosk { req } - /// Transfer NFT out of Kiosk that has been previously delegated to a base - /// Sui `Kiosk` + /// Transfer locked NFT out of Kiosk that has been previously delegated to + /// a base Sui `Kiosk` + /// + /// The transferred NFT is immediately locked in the target `Kiosk`. /// /// Requires that `UID` of sender was previously passed to either /// `auth_transfer` or `auth_exclusive_transfer`. @@ -589,11 +638,13 @@ module ob_kiosk::ob_kiosk { /// - Sender was not previously authorized for transfer or is not owner /// - NFT does not exist /// - Source is not an OriginByte `Kiosk` - public fun transfer_locked_nft( + public fun transfer_locked( source: &mut Kiosk, target: &mut Kiosk, nft_id: ID, entity_id: &UID, + paid: Coin, + transfer_policy: &sui::transfer_policy::TransferPolicy, ctx: &mut TxContext, ): TransferRequest { assert_version_and_upgrade(ext(source)); @@ -601,10 +652,49 @@ module ob_kiosk::ob_kiosk { check_entity_and_pop_ref(source, uid_to_address(entity_id), nft_id, ctx); let cap = pop_cap(source); - kiosk::list(source, &cap, nft_id, 0); + kiosk::list(source, &cap, nft_id, coin::value(&paid)); set_cap(source, cap); - let (nft, req) = kiosk::purchase(source, nft_id, coin::zero(ctx)); + let (nft, req) = kiosk::purchase(source, nft_id, paid); + deposit_locked(target, transfer_policy, nft, ctx); + + let req = transfer_request::from_sui(req, nft_id, uid_to_address(entity_id), ctx); + + req + } + + /// Transfer locked NFT out of Kiosk that has been previously delegated to + /// a base Sui `Kiosk` + /// + /// The transferred NFT is not locked in the target `Kiosk`. + /// + /// Requires that `UID` of sender was previously passed to either + /// `auth_transfer` or `auth_exclusive_transfer`. + /// + /// Will always work if transaction sender is the `Kiosk` owner. + /// + /// #### Panics + /// + /// - Sender was not previously authorized for transfer or is not owner + /// - NFT does not exist + /// - Source is not an OriginByte `Kiosk` + public fun transfer_unlocked( + source: &mut Kiosk, + target: &mut Kiosk, + nft_id: ID, + entity_id: &UID, + paid: Coin, + ctx: &mut TxContext, + ): TransferRequest { + assert_version_and_upgrade(ext(source)); + + check_entity_and_pop_ref(source, uid_to_address(entity_id), nft_id, ctx); + + let cap = pop_cap(source); + kiosk::list(source, &cap, nft_id, coin::value(&paid)); + set_cap(source, cap); + + let (nft, req) = kiosk::purchase(source, nft_id, paid); deposit(target, nft, ctx); let req = transfer_request::from_sui(req, nft_id, uid_to_address(entity_id), ctx); @@ -612,6 +702,17 @@ module ob_kiosk::ob_kiosk { req } + /// Deprecated, use `transfer_locked` instead + public fun transfer_locked_nft( + _source: &mut Kiosk, + _target: &mut Kiosk, + _nft_id: ID, + _entity_id: &UID, + _ctx: &mut TxContext, + ): TransferRequest { + abort(EDeprecatedApi) + } + /// Withdraw NFT from `Kiosk` without returning it /// /// Requires that `UID` of sender was previously passed to either @@ -819,6 +920,8 @@ module ob_kiosk::ob_kiosk { assert!(kiosk::has_access(self, &kiosk_cap), ENotOwner); assert!(!is_ob_kiosk(self), EKioskOriginByteVersion); + // Ensure that `uid_mut` will work + kiosk::set_allow_extensions(self, &kiosk_cap, true); let kiosk_ext = ext(self); df::add(kiosk_ext, VersionDfKey {}, VERSION); @@ -849,12 +952,21 @@ module ob_kiosk::ob_kiosk { assert_version_and_upgrade(ext(self)); assert_permission(self, ctx); - // Assert that Kiosk has NFT + register_nft_(self, nft_id); + } + + /// Create an `NftRef` entry for the NFT + /// + /// #### Panics + /// + /// Panics if `NftRef` already exists + fun register_nft_( + self: &mut Kiosk, + nft_id: ID, + ) { assert_has_nft(self, nft_id); assert!(!kiosk::is_listed(self, nft_id), ENftIsListedInBaseKiosk); - // Assert that Kiosk has no NftRef, which means the NFT was - // placed in the Kiosk before installing the OB extension let refs = nft_refs_mut(self); assert_missing_ref(refs, nft_id); @@ -1164,6 +1276,8 @@ module ob_kiosk::ob_kiosk { df::exists_(uid(self), NftRefsDfKey {}) } + /// Returns whether the current transaction sender can deposit into `Kiosk` + /// /// Either sender is owner or permissionless deposits of `T` enabled. public fun can_deposit(self: &mut Kiosk, ctx: &mut TxContext): bool { sender(ctx) == kiosk::owner(self) || can_deposit_permissionlessly(self) @@ -1189,7 +1303,6 @@ module ob_kiosk::ob_kiosk { /// Panics if `Kiosk` is not OriginByte `Kiosk` // // TODO: Replace with immutable API - // TODO: Consider removing test_only public fun nft_refs(self: &Kiosk): &Table { is_ob_kiosk_imut(self); df::borrow(uid(self), NftRefsDfKey {}) @@ -1221,6 +1334,11 @@ module ob_kiosk::ob_kiosk { assert!(is_permissionless(self), EKioskNotPermissionless); } + /// Asserts that the transaction sender may deposit into `Kiosk` + /// + /// #### Panics + /// + /// Panics if sender is not owner or `Kiosk` is not permissionless. public fun assert_can_deposit(self: &mut Kiosk, ctx: &mut TxContext) { assert!(can_deposit(self, ctx), ECannotDeposit); } @@ -1296,7 +1414,7 @@ module ob_kiosk::ob_kiosk { assert!(vec_set::size(&ref.auths) == 0, ENftAlreadyListed); } - /// Check whether NFT can be transferred by given authority + /// Check whether NFT can be transferred by given authority and remove the NftRef entry /// /// #### Panics /// @@ -1307,7 +1425,7 @@ module ob_kiosk::ob_kiosk { entity: address, nft_id: ID, ctx: &mut TxContext, - ) { + ): NftRef { let refs = nft_refs_mut(self); // NFT is being transferred - destroy the ref let ref: NftRef = table::remove(refs, nft_id); @@ -1316,6 +1434,7 @@ module ob_kiosk::ob_kiosk { is_owner(self, sender(ctx)) || vec_set::contains(&ref.auths, &entity), ENotAuthorized, ); + ref } /// Borrow `DepositSetting` field diff --git a/contracts/liquidity_layer_v1/sources/trading/orderbook.move b/contracts/liquidity_layer_v1/sources/trading/orderbook.move index d85fc72a..fc87ad86 100644 --- a/contracts/liquidity_layer_v1/sources/trading/orderbook.move +++ b/contracts/liquidity_layer_v1/sources/trading/orderbook.move @@ -88,9 +88,6 @@ module liquidity_layer_v1::orderbook { /// No order matches the given price level or ownership level const EOrderDoesNotExist: u64 = 6; - /// Market orders fail with this error if they cannot be filled - const EMarketOrderNotFilled: u64 = 6; - /// Trying to create an orderbook via a witness protected endpoint /// without TransferPolicy being registered with OriginByte const ENotOriginBytePolicy: u64 = 7; @@ -126,6 +123,12 @@ module liquidity_layer_v1::orderbook { /// Tried to call administrator protected endpoint while not administrator const ENotAdministrator: u64 = 16; + /// Market orders fail with this error if they cannot be filled + const EMarketOrderNotFilled: u64 = 17; + + /// Tried to resolve a `TradeIntermediate` field when one did not exist + const EUndefinedTradeIntermediate: u64 = 18; + // === Structs === /// Add this witness type to allowlists via @@ -728,20 +731,23 @@ module liquidity_layer_v1::orderbook { // === Finish trade === - /// When a bid is created and there's an ask with a lower price, then the - /// trade cannot be resolved immediately. - /// That's because we don't know the `Kiosk` ID up front in OB. - /// Conversely, when an ask is created, we don't know the `Kiosk` ID of the - /// buyer as the best bid can change at any time. + /// Executes a trade after orders have been matched /// - /// Therefore, orderbook creates [`TradeIntermediate`] which then has to be - /// permissionlessly resolved via this endpoint. + /// NFT will be deposited in target `Kiosk` without locking it. + /// + /// A separate trade execution step is necessary as we don't know the + /// target `Kiosk` upfront as the best bid or ask can change at any time. + /// + /// To resolve this, `Orderbook` creates a `TradeIntermediate` dynamic + /// field which can be permissionlessly resolved via this endpoint. /// /// See the documentation for `nft_protocol::transfer_request` to understand /// how to deal with the returned [`TransferRequest`] type. /// - /// * the buyer's kiosk must allow permissionless deposits of `T` unless - /// buyer is the signer + /// #### Panics + /// + /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless + /// buyer is the transaction sender. public fun finish_trade( book: &mut Orderbook, trade_id: ID, @@ -749,8 +755,31 @@ module liquidity_layer_v1::orderbook { buyer_kiosk: &mut Kiosk, ctx: &mut TxContext, ): TransferRequest { - // Version is being checked in function call `finish_trade_` - finish_trade_(book, trade_id, seller_kiosk, buyer_kiosk, ctx) + assert_version_and_upgrade(book); + + let trade = finalize_seller_side(book, trade_id, seller_kiosk); + let nft_id = trade.nft_id; + + let transfer_req = if (kiosk::is_locked(seller_kiosk, nft_id)) { + // This will cause the NFT to be transfered without locking + ob_kiosk::transfer_unlocked( + seller_kiosk, + buyer_kiosk, + nft_id, + &book.id, + coin::zero(ctx), + ctx, + ) + } else { + let price = balance::value(&trade.paid); + ob_kiosk::transfer_delegated( + seller_kiosk, buyer_kiosk, nft_id, &book.id, price, ctx, + ) + }; + + finalize_buyer_side(&mut transfer_req, trade, buyer_kiosk, ctx); + + transfer_req } public fun finish_trade_if_kiosks_match( @@ -760,7 +789,7 @@ module liquidity_layer_v1::orderbook { buyer_kiosk: &mut Kiosk, ctx: &mut TxContext ): Option> { - // Version is being checked in function call `finish_trade_` + assert_version_and_upgrade(book); let t = trade(book, trade_id); let kiosks_match = &t.seller_kiosk == &object::id(seller_kiosk) && &t.buyer_kiosk == &object::id(buyer_kiosk); @@ -772,6 +801,126 @@ module liquidity_layer_v1::orderbook { } } + /// Executes a trade after orders have been matched + /// + /// NFT will be deposited in target `Kiosk` and immediately locked. + /// + /// A separate trade execution step is necessary as we don't know the + /// target `Kiosk` upfront as the best bid or ask can change at any time. + /// + /// To resolve this, `Orderbook` creates a `TradeIntermediate` dynamic + /// field which can be permissionlessly resolved via this endpoint. + /// + /// See the documentation for `nft_protocol::transfer_request` to understand + /// how to deal with the returned [`TransferRequest`] type. + /// + /// #### Panics + /// + /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless + /// buyer is the transaction sender. + public fun finish_trade_locked( + book: &mut Orderbook, + trade_id: ID, + seller_kiosk: &mut Kiosk, + buyer_kiosk: &mut Kiosk, + transfer_policy: &sui::transfer_policy::TransferPolicy, + ctx: &mut TxContext, + ): TransferRequest { + assert_version_and_upgrade(book); + + let trade = finalize_seller_side(book, trade_id, seller_kiosk); + let nft_id = trade.nft_id; + + let transfer_req = if (kiosk::is_locked(seller_kiosk, nft_id)) { + // This will cause the NFT to be transfered and locked + ob_kiosk::transfer_locked( + seller_kiosk, + buyer_kiosk, + nft_id, + &book.id, + coin::zero(ctx), + transfer_policy, + ctx, + ) + } else { + let price = balance::value(&trade.paid); + ob_kiosk::transfer_delegated_locked( + seller_kiosk, + buyer_kiosk, + nft_id, + &book.id, + price, + transfer_policy, + ctx, + ) + }; + + finalize_buyer_side(&mut transfer_req, trade, buyer_kiosk, ctx); + + transfer_req + } + + /// Finalizes trade on the seller side by resolving `TradeIntermediate` + /// + /// #### Panics + /// + /// - `TradeIntermediate` did not exist for given type and `ID` + /// - Seller `Kiosk` did not match trade + fun finalize_seller_side( + book: &mut Orderbook, + trade_id: ID, + seller_kiosk: &mut Kiosk, + ): TradeIntermediate { + let trade_key = TradeIntermediateDfKey { trade_id }; + let trade_opt = df::remove_if_exists(&mut book.id, trade_key); + assert!(option::is_some(&trade_opt), EUndefinedTradeIntermediate); + let trade: TradeIntermediate = option::destroy_some(trade_opt); + + // Assert we are resolving from the correct `Kiosk` + assert!( + trade.seller_kiosk == object::id(seller_kiosk), + EKioskIdMismatch, + ); + + trade + } + + /// Finalizes trade on the buyer side by consuming `TradeIntermediate` + /// + /// #### Panics + /// + /// - Buyer `Kiosk` did not match trade + fun finalize_buyer_side( + transfer_req: &mut TransferRequest, + trade: TradeIntermediate, + buyer_kiosk: &Kiosk, + ctx: &mut TxContext, + ) { + assert!( + trade.buyer_kiosk == object::id(buyer_kiosk), + EKioskIdMismatch, + ); + + let TradeIntermediate { + id, + nft_id: _, + seller_kiosk: _, + paid, + seller, + buyer: _, + buyer_kiosk: _, + commission: commission, + } = trade; + + object::delete(id); + + // Sell-side commission gets transferred to the sell-side intermediary + trading::transfer_ask_commission(&mut commission, &mut paid, ctx); + + transfer_request::set_paid(transfer_req, paid, seller); + ob_kiosk::set_transfer_request_auth(transfer_req, &Witness {}); + } + // === Create orderbook === /// NFTs of type `T` to be traded, and `F`ungible `T`oken to be @@ -1834,68 +1983,6 @@ module liquidity_layer_v1::orderbook { transfer_req } - fun finish_trade_( - book: &mut Orderbook, - trade_id: ID, - seller_kiosk: &mut Kiosk, - buyer_kiosk: &mut Kiosk, - ctx: &mut TxContext, - ): TransferRequest { - assert_version_and_upgrade(book); - - let trade = df::remove( - &mut book.id, TradeIntermediateDfKey { trade_id } - ); - - let TradeIntermediate { - id, - nft_id, - seller_kiosk: _, - paid, - seller, - buyer: _, - buyer_kiosk: expected_buyer_kiosk_id, - commission: maybe_commission, - } = trade; - - object::delete(id); - - let price = balance::value(&paid); - - assert!( - expected_buyer_kiosk_id == object::id(buyer_kiosk), EKioskIdMismatch, - ); - - // Sell-side commission gets transferred to the sell-side intermediary - trading::transfer_ask_commission(&mut maybe_commission, &mut paid, ctx); - - let transfer_req = if (kiosk::is_locked(seller_kiosk, nft_id)) { - ob_kiosk::transfer_locked_nft( - seller_kiosk, - buyer_kiosk, - nft_id, - &book.id, - ctx, - ) - } else { - ob_kiosk::transfer_delegated( - seller_kiosk, - buyer_kiosk, - nft_id, - &book.id, - price, - ctx, - ) - }; - - transfer_request::set_paid( - &mut transfer_req, paid, seller, - ); - ob_kiosk::set_transfer_request_auth(&mut transfer_req, &Witness {}); - - transfer_req - } - /// Finds an ask of a given NFT advertized for the given price. Removes it /// from the asks vector preserving order and returns it. fun remove_ask(asks: &mut CBTree>, price: u64, nft_id: ID): Ask { From ec2006b160f6706df64aa2fd8ad32b45f827e022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=81abor?= Date: Fri, 14 Jul 2023 16:28:38 +0200 Subject: [PATCH 2/9] Inherit locked state --- .../sources/trading/orderbook.move | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/contracts/liquidity_layer_v1/sources/trading/orderbook.move b/contracts/liquidity_layer_v1/sources/trading/orderbook.move index fc87ad86..a6b28942 100644 --- a/contracts/liquidity_layer_v1/sources/trading/orderbook.move +++ b/contracts/liquidity_layer_v1/sources/trading/orderbook.move @@ -860,6 +860,61 @@ module liquidity_layer_v1::orderbook { transfer_req } + /// Executes a trade after orders have been matched + /// + /// Inherits the NFTs locked state from the source `Kiosk` to the target. + /// - If NFT was locked in source it will be locked in target + /// - If NFT was unlocked in source it will be locked in target + /// + /// A separate trade execution step is necessary as we don't know the + /// target `Kiosk` upfront as the best bid or ask can change at any time. + /// + /// To resolve this, `Orderbook` creates a `TradeIntermediate` dynamic + /// field which can be permissionlessly resolved via this endpoint. + /// + /// See the documentation for `nft_protocol::transfer_request` to understand + /// how to deal with the returned [`TransferRequest`] type. + /// + /// #### Panics + /// + /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless + /// buyer is the transaction sender. + public fun finish_trade_inherit( + book: &mut Orderbook, + trade_id: ID, + seller_kiosk: &mut Kiosk, + buyer_kiosk: &mut Kiosk, + transfer_policy: &sui::transfer_policy::TransferPolicy, + ctx: &mut TxContext, + ): TransferRequest { + assert_version_and_upgrade(book); + + let trade = finalize_seller_side(book, trade_id, seller_kiosk); + let nft_id = trade.nft_id; + + let transfer_req = if (kiosk::is_locked(seller_kiosk, nft_id)) { + // This will cause the NFT to be transfered and locked + ob_kiosk::transfer_locked( + seller_kiosk, + buyer_kiosk, + nft_id, + &book.id, + coin::zero(ctx), + transfer_policy, + ctx, + ) + } else { + let price = balance::value(&trade.paid); + ob_kiosk::transfer_delegated( + seller_kiosk, buyer_kiosk, nft_id, &book.id, price, ctx, + ) + }; + + finalize_buyer_side(&mut transfer_req, trade, buyer_kiosk, ctx); + + transfer_req + } + /// Finalizes trade on the seller side by resolving `TradeIntermediate` /// /// #### Panics From c1d5462869749c34241936bd75d0d8262702730c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=81abor?= Date: Fri, 14 Jul 2023 16:58:03 +0200 Subject: [PATCH 3/9] Initial tests --- .../liquidity_layer_v1/tests/locked.move | 24 +++++++++ .../orderbook/orderbook.move | 49 ------------------- 2 files changed, 24 insertions(+), 49 deletions(-) create mode 100644 contracts/liquidity_layer_v1/tests/locked.move diff --git a/contracts/liquidity_layer_v1/tests/locked.move b/contracts/liquidity_layer_v1/tests/locked.move new file mode 100644 index 00000000..6d960ae0 --- /dev/null +++ b/contracts/liquidity_layer_v1/tests/locked.move @@ -0,0 +1,24 @@ +#[test_only] +/// Tests trading compatibility with locked NFTs +module liquidity_layer_v1::test_orderbook_locked { + #[test] + fun test_transfer_to_unlocked() {} + + #[test] + fun test_transfer_to_unlocked_non_ob() {} + + #[test] + fun test_transfer_to_locked() {} + + #[test] + fun test_transfer_to_locked_non_ob() {} + + #[test] + fun test_transfer_listed_non_ob() {} + + #[test] + fun test_transfer_exclusively_listed_non_ob() {} + + #[test] + fun test_request_payment() {} +} \ No newline at end of file diff --git a/contracts/tests/tests/liquidity_layer_v1/orderbook/orderbook.move b/contracts/tests/tests/liquidity_layer_v1/orderbook/orderbook.move index 373227e1..ff08fb12 100644 --- a/contracts/tests/tests/liquidity_layer_v1/orderbook/orderbook.move +++ b/contracts/tests/tests/liquidity_layer_v1/orderbook/orderbook.move @@ -148,14 +148,12 @@ module ob_tests::orderbook_v1 { let scenario = test_scenario::begin(creator()); // 1. Create Collection, TransferPolicy and Orderbook - let (collection, mint_cap) = test_utils::init_collection_foo(ctx(&mut scenario)); let publisher = test_utils::get_publisher(ctx(&mut scenario)); let (tx_policy, policy_cap) = test_utils::init_transfer_policy(&publisher, ctx(&mut scenario)); let dw = witness::test_dw(); test_utils::create_orderbook_v1(dw, &tx_policy, &mut scenario); - transfer::public_share_object(collection); transfer::public_share_object(tx_policy); // 3. Create Buyer Kiosk @@ -217,7 +215,6 @@ module ob_tests::orderbook_v1 { coin::burn_for_testing(coin); transfer::public_transfer(publisher, creator()); - transfer::public_transfer(mint_cap, creator()); transfer::public_transfer(policy_cap, creator()); test_scenario::return_shared(tx_policy); test_scenario::return_shared(seller_kiosk); @@ -231,14 +228,12 @@ module ob_tests::orderbook_v1 { let scenario = test_scenario::begin(creator()); // 1. Create Collection, TransferPolicy and Orderbook - let (collection, mint_cap) = test_utils::init_collection_foo(ctx(&mut scenario)); let publisher = test_utils::get_publisher(ctx(&mut scenario)); let (tx_policy, policy_cap) = test_utils::init_transfer_policy(&publisher, ctx(&mut scenario)); let dw = witness::test_dw(); test_utils::create_orderbook_v1(dw, &tx_policy, &mut scenario); - transfer::public_share_object(collection); transfer::public_share_object(tx_policy); // 3. Create Buyer Kiosk @@ -336,7 +331,6 @@ module ob_tests::orderbook_v1 { coin::burn_for_testing(bid_commission); coin::burn_for_testing(ask_commission); transfer::public_transfer(publisher, creator()); - transfer::public_transfer(mint_cap, creator()); transfer::public_transfer(policy_cap, creator()); test_scenario::return_shared(tx_policy); test_scenario::return_shared(seller_kiosk); @@ -607,13 +601,11 @@ module ob_tests::orderbook_v1 { let scenario = test_scenario::begin(creator()); // 1. Create Collection, TransferPolicy and Orderbook - let (collection, mint_cap) = test_utils::init_collection_foo(ctx(&mut scenario)); let publisher = test_utils::get_publisher(ctx(&mut scenario)); let (tx_policy, policy_cap) = transfer_policy::new(&publisher, ctx(&mut scenario)); test_utils::create_external_orderbook_v1(&tx_policy, &mut scenario); - transfer::public_share_object(collection); transfer::public_share_object(tx_policy); // 3. Create Buyer Kiosk @@ -676,7 +668,6 @@ module ob_tests::orderbook_v1 { coin::burn_for_testing(coin); transfer::public_transfer(publisher, creator()); - transfer::public_transfer(mint_cap, creator()); transfer::public_transfer(policy_cap, creator()); test_scenario::return_shared(tx_policy); test_scenario::return_shared(seller_kiosk); @@ -690,13 +681,11 @@ module ob_tests::orderbook_v1 { let scenario = test_scenario::begin(creator()); // 1. Create Collection, TransferPolicy and Orderbook - let (collection, mint_cap) = test_utils::init_collection_foo(ctx(&mut scenario)); let publisher = test_utils::get_publisher(ctx(&mut scenario)); let (tx_policy, policy_cap) = transfer_policy::new(&publisher, ctx(&mut scenario)); test_utils::create_external_orderbook_v1(&tx_policy, &mut scenario); - transfer::public_share_object(collection); transfer::public_share_object(tx_policy); // 3. Create Buyer Kiosk @@ -772,7 +761,6 @@ module ob_tests::orderbook_v1 { coin::burn_for_testing(coin); transfer::public_transfer(publisher, buyer()); - transfer::public_transfer(mint_cap, creator()); transfer::public_transfer(policy_cap, creator()); test_scenario::return_shared(tx_policy); test_scenario::return_shared(seller_kiosk); @@ -786,14 +774,12 @@ module ob_tests::orderbook_v1 { let scenario = test_scenario::begin(creator()); // 1. Create Collection, TransferPolicy and Orderbook - let (collection, mint_cap) = test_utils::init_collection_foo(ctx(&mut scenario)); let publisher = test_utils::get_publisher(ctx(&mut scenario)); let (tx_policy, policy_cap) = test_utils::init_transfer_policy(&publisher, ctx(&mut scenario)); let dw = witness::test_dw(); test_utils::create_orderbook_v1(dw, &tx_policy, &mut scenario); - transfer::public_share_object(collection); transfer::public_share_object(tx_policy); // 3. Create Buyer Kiosk @@ -879,7 +865,6 @@ module ob_tests::orderbook_v1 { coin::burn_for_testing(coin); transfer::public_transfer(publisher, creator()); - transfer::public_transfer(mint_cap, creator()); transfer::public_transfer(policy_cap, creator()); // test_scenario::return_shared(tx_policy); test_scenario::return_shared(seller_kiosk); @@ -893,14 +878,12 @@ module ob_tests::orderbook_v1 { let scenario = test_scenario::begin(creator()); // 1. Create Collection, TransferPolicy and Orderbook - let (collection, mint_cap) = test_utils::init_collection_foo(ctx(&mut scenario)); let publisher = test_utils::get_publisher(ctx(&mut scenario)); let (tx_policy, policy_cap) = test_utils::init_transfer_policy(&publisher, ctx(&mut scenario)); let dw = witness::test_dw(); test_utils::create_orderbook_v1(dw, &tx_policy, &mut scenario); - transfer::public_share_object(collection); transfer::public_share_object(tx_policy); // 3. Create Buyer Kiosk @@ -992,7 +975,6 @@ module ob_tests::orderbook_v1 { coin::burn_for_testing(coin); transfer::public_transfer(publisher, creator()); - transfer::public_transfer(mint_cap, creator()); transfer::public_transfer(policy_cap, creator()); test_scenario::return_shared(seller_kiosk); test_scenario::return_shared(buyer_kiosk); @@ -1005,15 +987,12 @@ module ob_tests::orderbook_v1 { let scenario = test_scenario::begin(creator()); // 1. Create Collection, TransferPolicy and Orderbook - let (collection, mint_cap) = test_utils::init_collection_foo(ctx(&mut scenario)); let publisher = test_utils::get_publisher(ctx(&mut scenario)); let (tx_policy, policy_cap) = test_utils::init_transfer_policy(&publisher, ctx(&mut scenario)); let dw = witness::test_dw(); test_utils::create_orderbook_v1(dw, &tx_policy, &mut scenario); - - transfer::public_share_object(collection); transfer::public_share_object(tx_policy); // 3. Create Buyer Kiosk @@ -1116,7 +1095,6 @@ module ob_tests::orderbook_v1 { coin::burn_for_testing(coin); transfer::public_transfer(publisher, creator()); - transfer::public_transfer(mint_cap, creator()); transfer::public_transfer(policy_cap, creator()); test_scenario::return_shared(seller_kiosk); test_scenario::return_shared(buyer_kiosk); @@ -1129,14 +1107,12 @@ module ob_tests::orderbook_v1 { let scenario = test_scenario::begin(creator()); // 1. Create Collection, TransferPolicy and Orderbook - let (collection, mint_cap) = test_utils::init_collection_foo(ctx(&mut scenario)); let publisher = test_utils::get_publisher(ctx(&mut scenario)); let (tx_policy, policy_cap) = test_utils::init_transfer_policy(&publisher, ctx(&mut scenario)); let dw = witness::test_dw(); test_utils::create_orderbook_v1(dw, &tx_policy, &mut scenario); - transfer::public_share_object(collection); transfer::public_share_object(tx_policy); // 3. Create Buyer Kiosk @@ -1237,7 +1213,6 @@ module ob_tests::orderbook_v1 { coin::burn_for_testing(coin); transfer::public_transfer(publisher, creator()); - transfer::public_transfer(mint_cap, creator()); transfer::public_transfer(policy_cap, creator()); test_scenario::return_shared(seller_kiosk); test_scenario::return_shared(buyer_kiosk); @@ -1250,14 +1225,12 @@ module ob_tests::orderbook_v1 { let scenario = test_scenario::begin(creator()); // 1. Create Collection, TransferPolicy and Orderbook - let (collection, mint_cap) = test_utils::init_collection_foo(ctx(&mut scenario)); let publisher = test_utils::get_publisher(ctx(&mut scenario)); let (tx_policy, policy_cap) = test_utils::init_transfer_policy(&publisher, ctx(&mut scenario)); let dw = witness::test_dw(); test_utils::create_orderbook_v1(dw, &tx_policy, &mut scenario); - transfer::public_share_object(collection); transfer::public_share_object(tx_policy); // 3. Create Buyer Kiosk @@ -1336,7 +1309,6 @@ module ob_tests::orderbook_v1 { coin::burn_for_testing(coin); transfer::public_transfer(publisher, creator()); - transfer::public_transfer(mint_cap, creator()); transfer::public_transfer(policy_cap, creator()); test_scenario::return_shared(seller_kiosk); test_scenario::return_shared(buyer_kiosk); @@ -1349,14 +1321,12 @@ module ob_tests::orderbook_v1 { let scenario = test_scenario::begin(creator()); // 1. Create Collection, TransferPolicy and Orderbook - let (collection, mint_cap) = test_utils::init_collection_foo(ctx(&mut scenario)); let publisher = test_utils::get_publisher(ctx(&mut scenario)); let (tx_policy, policy_cap) = test_utils::init_transfer_policy(&publisher, ctx(&mut scenario)); let dw = witness::test_dw(); test_utils::create_orderbook_v1(dw, &tx_policy, &mut scenario); - transfer::public_share_object(collection); transfer::public_share_object(tx_policy); // 3. Create Buyer Kiosk @@ -1450,7 +1420,6 @@ module ob_tests::orderbook_v1 { coin::burn_for_testing(coin); transfer::public_transfer(publisher, creator()); - transfer::public_transfer(mint_cap, creator()); transfer::public_transfer(policy_cap, creator()); test_scenario::return_shared(seller_kiosk); test_scenario::return_shared(buyer_kiosk); @@ -1463,14 +1432,12 @@ module ob_tests::orderbook_v1 { let scenario = test_scenario::begin(creator()); // 1. Create Collection, TransferPolicy and Orderbook - let (collection, mint_cap) = test_utils::init_collection_foo(ctx(&mut scenario)); let publisher = test_utils::get_publisher(ctx(&mut scenario)); let (tx_policy, policy_cap) = test_utils::init_transfer_policy(&publisher, ctx(&mut scenario)); let dw = witness::test_dw(); test_utils::create_orderbook_v1(dw, &tx_policy, &mut scenario); - transfer::public_share_object(collection); transfer::public_share_object(tx_policy); // 3. Create Buyer Kiosk @@ -1555,7 +1522,6 @@ module ob_tests::orderbook_v1 { coin::burn_for_testing(coin); transfer::public_transfer(publisher, creator()); - transfer::public_transfer(mint_cap, creator()); transfer::public_transfer(policy_cap, creator()); test_scenario::return_shared(seller_kiosk); test_scenario::return_shared(buyer_kiosk); @@ -1568,14 +1534,12 @@ module ob_tests::orderbook_v1 { let scenario = test_scenario::begin(creator()); // 1. Create Collection, TransferPolicy and Orderbook - let (collection, mint_cap) = test_utils::init_collection_foo(ctx(&mut scenario)); let publisher = test_utils::get_publisher(ctx(&mut scenario)); let (tx_policy, policy_cap) = test_utils::init_transfer_policy(&publisher, ctx(&mut scenario)); let dw = witness::test_dw(); test_utils::create_orderbook_v1(dw, &tx_policy, &mut scenario); - transfer::public_share_object(collection); transfer::public_share_object(tx_policy); // 3. Create Buyer Kiosk @@ -1677,7 +1641,6 @@ module ob_tests::orderbook_v1 { coin::burn_for_testing(coin); transfer::public_transfer(publisher, creator()); - transfer::public_transfer(mint_cap, creator()); transfer::public_transfer(policy_cap, creator()); test_scenario::return_shared(seller_kiosk); test_scenario::return_shared(buyer_kiosk); @@ -1690,7 +1653,6 @@ module ob_tests::orderbook_v1 { let scenario = test_scenario::begin(creator()); // 1. Create Collection, TransferPolicy and Orderbook - let (collection, mint_cap) = test_utils::init_collection_foo(ctx(&mut scenario)); let publisher = test_utils::get_publisher(ctx(&mut scenario)); let (tx_policy, policy_cap) = test_utils::init_transfer_policy(&publisher, ctx(&mut scenario)); @@ -1706,7 +1668,6 @@ module ob_tests::orderbook_v1 { orderbook::change_tick_size(dw, &mut ob, 1); orderbook::share(ob); - transfer::public_share_object(collection); transfer::public_share_object(tx_policy); // 2. Insert time lock @@ -1749,7 +1710,6 @@ module ob_tests::orderbook_v1 { clock::destroy_for_testing(clock); transfer::public_transfer(publisher, creator()); - transfer::public_transfer(mint_cap, creator()); transfer::public_transfer(policy_cap, creator()); test_scenario::return_shared(book); test_scenario::end(scenario); @@ -1761,7 +1721,6 @@ module ob_tests::orderbook_v1 { let scenario = test_scenario::begin(creator()); // 1. Create Collection, TransferPolicy and Orderbook - let (collection, mint_cap) = test_utils::init_collection_foo(ctx(&mut scenario)); let publisher = test_utils::get_publisher(ctx(&mut scenario)); let (tx_policy, policy_cap) = test_utils::init_transfer_policy(&publisher, ctx(&mut scenario)); @@ -1777,7 +1736,6 @@ module ob_tests::orderbook_v1 { orderbook::change_tick_size(dw, &mut ob, 1); orderbook::share(ob); - transfer::public_share_object(collection); transfer::public_share_object(tx_policy); // 2. Insert time lock @@ -1796,7 +1754,6 @@ module ob_tests::orderbook_v1 { clock::destroy_for_testing(clock); transfer::public_transfer(publisher, creator()); - transfer::public_transfer(mint_cap, creator()); transfer::public_transfer(policy_cap, creator()); test_scenario::return_shared(book); test_scenario::end(scenario); @@ -1808,7 +1765,6 @@ module ob_tests::orderbook_v1 { let scenario = test_scenario::begin(creator()); // 1. Create Collection, TransferPolicy and Orderbook - let (collection, mint_cap) = test_utils::init_collection_foo(ctx(&mut scenario)); let publisher = test_utils::get_publisher(ctx(&mut scenario)); let (tx_policy, policy_cap) = test_utils::init_transfer_policy(&publisher, ctx(&mut scenario)); @@ -1816,7 +1772,6 @@ module ob_tests::orderbook_v1 { test_utils::create_orderbook_v1(dw, &tx_policy, &mut scenario); - transfer::public_share_object(collection); transfer::public_share_object(tx_policy); // 3. Create Buyer Kiosk @@ -1883,7 +1838,6 @@ module ob_tests::orderbook_v1 { coin::burn_for_testing(coin); transfer::public_transfer(publisher, creator()); - transfer::public_transfer(mint_cap, creator()); transfer::public_transfer(policy_cap, creator()); test_scenario::return_shared(bid); test_scenario::return_shared(tx_policy); @@ -1899,7 +1853,6 @@ module ob_tests::orderbook_v1 { let scenario = test_scenario::begin(creator()); // 1. Create Collection, TransferPolicy and Orderbook - let (collection, mint_cap) = test_utils::init_collection_foo(ctx(&mut scenario)); let publisher = test_utils::get_publisher(ctx(&mut scenario)); let (tx_policy, policy_cap) = test_utils::init_transfer_policy(&publisher, ctx(&mut scenario)); @@ -1915,7 +1868,6 @@ module ob_tests::orderbook_v1 { orderbook::change_tick_size(dw, &mut ob, 1); orderbook::share(ob); - transfer::public_share_object(collection); transfer::public_share_object(tx_policy); // 2. Insert administrator @@ -1953,7 +1905,6 @@ module ob_tests::orderbook_v1 { orderbook::remove_start_time_as_administrator(&mut book, ctx(&mut scenario)); transfer::public_transfer(publisher, creator()); - transfer::public_transfer(mint_cap, creator()); transfer::public_transfer(policy_cap, creator()); test_scenario::return_shared(book); From 7848b8119215755b79877c33c017b2ea30e2541c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=81abor?= Date: Sat, 15 Jul 2023 18:15:28 +0200 Subject: [PATCH 4/9] Test cases and cleanup --- contracts/kiosk/sources/kiosk/ob_kiosk.move | 151 +++++--- .../sources/trading/orderbook.move | 15 +- .../liquidity_layer_v1/tests/locked.move | 345 +++++++++++++++++- 3 files changed, 445 insertions(+), 66 deletions(-) diff --git a/contracts/kiosk/sources/kiosk/ob_kiosk.move b/contracts/kiosk/sources/kiosk/ob_kiosk.move index 02d7a6fa..2b71b2b3 100644 --- a/contracts/kiosk/sources/kiosk/ob_kiosk.move +++ b/contracts/kiosk/sources/kiosk/ob_kiosk.move @@ -494,8 +494,7 @@ module ob_kiosk::ob_kiosk { assert_version_and_upgrade(ext(source)); assert_permission(source, ctx); - let refs = nft_refs_mut(source); - let ref = table::remove(refs, nft_id); + let ref = deregister_nft_(source, nft_id); assert_ref_not_exclusively_listed(&ref); let cap = pop_cap(source); @@ -649,17 +648,10 @@ module ob_kiosk::ob_kiosk { ): TransferRequest { assert_version_and_upgrade(ext(source)); - check_entity_and_pop_ref(source, uid_to_address(entity_id), nft_id, ctx); - - let cap = pop_cap(source); - kiosk::list(source, &cap, nft_id, coin::value(&paid)); - set_cap(source, cap); - - let (nft, req) = kiosk::purchase(source, nft_id, paid); + let (nft, req) = transfer_locked_nft_( + source, nft_id, uid_to_address(entity_id), paid, ctx, + ); deposit_locked(target, transfer_policy, nft, ctx); - - let req = transfer_request::from_sui(req, nft_id, uid_to_address(entity_id), ctx); - req } @@ -688,17 +680,10 @@ module ob_kiosk::ob_kiosk { ): TransferRequest { assert_version_and_upgrade(ext(source)); - check_entity_and_pop_ref(source, uid_to_address(entity_id), nft_id, ctx); - - let cap = pop_cap(source); - kiosk::list(source, &cap, nft_id, coin::value(&paid)); - set_cap(source, cap); - - let (nft, req) = kiosk::purchase(source, nft_id, paid); + let (nft, req) = transfer_locked_nft_( + source, nft_id, uid_to_address(entity_id), paid, ctx, + ); deposit(target, nft, ctx); - - let req = transfer_request::from_sui(req, nft_id, uid_to_address(entity_id), ctx); - req } @@ -831,8 +816,7 @@ module ob_kiosk::ob_kiosk { assert!(kiosk::owner(source) != PermissionlessAddr, ENotAuthorized); assert!(kiosk::owner(source) == kiosk::owner(target), ENotOwner); - let refs = nft_refs_mut(source); - let ref = table::remove(refs, nft_id); + let ref = deregister_nft_(source, nft_id); assert_ref_not_exclusively_listed(&ref); let cap = pop_cap(source); @@ -976,6 +960,20 @@ module ob_kiosk::ob_kiosk { }); } + /// Pop `NftRef` entry for the NFT + /// + /// #### Panics + /// + /// Panics if `NftRef` does not exist + fun deregister_nft_( + self: &mut Kiosk, + nft_id: ID, + ): NftRef { + let refs = nft_refs_mut(self); + assert!(table::contains(refs, nft_id), EMissingNft); + table::remove(refs, nft_id) + } + // === Private Functions === /// Initializes a transfer transaction @@ -985,6 +983,7 @@ module ob_kiosk::ob_kiosk { /// - Originator is not authorized to withdraw and transaction sender is /// not owner. /// - NFT does not exist + /// - NFT is locked fun transfer_nft_( self: &mut Kiosk, nft_id: ID, @@ -996,6 +995,26 @@ module ob_kiosk::ob_kiosk { (nft, transfer_request::new(nft_id, originator, object::id(self), price, ctx)) } + /// Initializes a transfer transaction for locked NFT + /// + /// #### Panics + /// + /// - Originator is not authorized to withdraw and transaction sender is + /// not owner. + /// - NFT does not exist + /// - NFT is not locked + fun transfer_locked_nft_( + self: &mut Kiosk, + nft_id: ID, + originator: address, + paid: Coin, + ctx: &mut TxContext, + ): (T, TransferRequest) { + // TODO: Merge with `transfer_nft_` + let (nft, req) = remove_locked_nft(self, nft_id, originator, paid, ctx); + (nft, transfer_request::from_sui(req, nft_id, originator, ctx)) + } + /// Initializes a withdrawal transaction /// /// #### Panics @@ -1003,6 +1022,7 @@ module ob_kiosk::ob_kiosk { /// - Originator is not authorized to withdraw and transaction sender is /// not owner. /// - NFT does not exist + /// - NFT is locked fun withdraw_nft_( self: &mut Kiosk, nft_id: ID, @@ -1013,20 +1033,23 @@ module ob_kiosk::ob_kiosk { (nft, withdraw_request::new(originator, ctx)) } - /// Checks that originator is authorized to withdraw NFT + /// Checks that originator is authorized to withdraw NFT and returns the + /// NFT /// /// #### Panics /// /// - Originator is not authorized to withdraw and transaction sender is /// not owner. /// - NFT does not exist + /// - NFT is locked fun remove_nft( self: &mut Kiosk, nft_id: ID, originator: address, ctx: &mut TxContext, ): T { - check_entity_and_pop_ref(self, originator, nft_id, ctx); + assert_can_transfer(self, nft_id, originator, ctx); + deregister_nft_(self, nft_id); let cap = pop_cap(self); let nft = kiosk::take(self, &cap, nft_id); @@ -1035,6 +1058,32 @@ module ob_kiosk::ob_kiosk { nft } + /// Checks that originator is authorized to withdraw NFT and returns the + /// NFT + /// + /// #### Panics + /// + /// - Originator is not authorized to withdraw and transaction sender is + /// not owner. + /// - NFT does not exist + /// - NFT is not locked + fun remove_locked_nft( + self: &mut Kiosk, + nft_id: ID, + originator: address, + paid: Coin, + ctx: &mut TxContext + ): (T, sui::transfer_policy::TransferRequest) { + assert_can_transfer(self, nft_id, originator, ctx); + deregister_nft_(self, nft_id); + + let cap = pop_cap(self); + kiosk::list(self, &cap, nft_id, coin::value(&paid)); + set_cap(self, cap); + + kiosk::purchase(self, nft_id, paid) + } + // === Request Auth === /// Proves access to given type `Auth`. @@ -1283,12 +1332,20 @@ module ob_kiosk::ob_kiosk { sender(ctx) == kiosk::owner(self) || can_deposit_permissionlessly(self) } + /// Returns whether `DepositSettings` allow for permissionless deposits. + /// + /// Either `Kiosk` is permissionless, any deposits are allowed, or `T` was + /// explicitly whitelisted. + /// + /// If `Kiosk` is not an OriginByte `Kiosk` then we assume that + /// permissionless deposits are allowed and trust that the base `Kiosk` + /// manages this itself. public fun can_deposit_permissionlessly(self: &mut Kiosk): bool { - if (is_permissionless(self)) { + if (!is_ob_kiosk(self) || is_permissionless(self)) { return true }; - let settings = deposit_setting_mut(self); + let settings = deposit_setting(self); settings.enable_any_deposit || vec_set::contains( &settings.collections_with_enabled_deposits, @@ -1314,7 +1371,6 @@ module ob_kiosk::ob_kiosk { /// /// Panics if `Kiosk` is not OriginByte `Kiosk` or if NFT does not exist. // - // TODO: Replace with immutable API // TODO: Consider making it public fun nft_ref(self: &Kiosk, nft_id: ID): &NftRef { let refs = nft_refs(self); @@ -1347,6 +1403,20 @@ module ob_kiosk::ob_kiosk { assert!(can_deposit_permissionlessly(self), EPermissionlessDepositsDisabled); } + /// Asserts that current transaction sender may transfer an NFT out of `Kiosk` + fun assert_can_transfer( + self: &Kiosk, + nft_id: ID, + entity: address, + ctx: &mut TxContext + ) { + let ref = nft_ref(self, nft_id); + assert!( + is_owner(self, tx_context::sender(ctx)) || vec_set::contains(&ref.auths, &entity), + ENotAuthorized, + ); + } + /// Asserts that owner is provided address /// /// #### Panics @@ -1414,29 +1484,6 @@ module ob_kiosk::ob_kiosk { assert!(vec_set::size(&ref.auths) == 0, ENftAlreadyListed); } - /// Check whether NFT can be transferred by given authority and remove the NftRef entry - /// - /// #### Panics - /// - /// Panics if `address` was not authorized to transfer and transaction - /// sender is not the `Kiosk` owner. - fun check_entity_and_pop_ref( - self: &mut Kiosk, - entity: address, - nft_id: ID, - ctx: &mut TxContext, - ): NftRef { - let refs = nft_refs_mut(self); - // NFT is being transferred - destroy the ref - let ref: NftRef = table::remove(refs, nft_id); - // Sender is owner or entity is an authority - assert!( - is_owner(self, sender(ctx)) || vec_set::contains(&ref.auths, &entity), - ENotAuthorized, - ); - ref - } - /// Borrow `DepositSetting` field /// /// #### Panics diff --git a/contracts/liquidity_layer_v1/sources/trading/orderbook.move b/contracts/liquidity_layer_v1/sources/trading/orderbook.move index a6b28942..cc11e9ea 100644 --- a/contracts/liquidity_layer_v1/sources/trading/orderbook.move +++ b/contracts/liquidity_layer_v1/sources/trading/orderbook.move @@ -1546,14 +1546,15 @@ module liquidity_layer_v1::orderbook { // === Priv fns === - /// * buyer kiosk must be in Originbyte ecosystem - /// * sender must be owner of buyer kiosk - /// * kiosk must allow permissionless deposits of `T` + /// Creates a bid and attempts to immediately execute it /// - /// Either `TradeIntermediate` is shared, or bid is added to the state. + /// Returns `TradeInfo` which can be used to to call `finish_trade` + /// endpoints. /// - /// Returns `Some` with amount if matched. - /// The amount is always equal or less than price. + /// #### Panics + /// + /// - Transaction sender is not owner of `Kiosk` + /// - `Kiosk` does not allow permissionless deposits of `T` fun create_bid_( book: &mut Orderbook, buyer_kiosk: &mut Kiosk, @@ -1564,7 +1565,6 @@ module liquidity_layer_v1::orderbook { ): Option { assert_version_and_upgrade(book); assert_tick_level(price, book.tick_size); - ob_kiosk::assert_is_ob_kiosk(buyer_kiosk); ob_kiosk::assert_permission(buyer_kiosk, ctx); ob_kiosk::assert_can_deposit_permissionlessly(buyer_kiosk); @@ -1578,7 +1578,6 @@ module liquidity_layer_v1::orderbook { (false, 0) } else { let lowest_ask_price = crit_bit::min_key(asks); - (lowest_ask_price <= price, lowest_ask_price) }; diff --git a/contracts/liquidity_layer_v1/tests/locked.move b/contracts/liquidity_layer_v1/tests/locked.move index 6d960ae0..da532334 100644 --- a/contracts/liquidity_layer_v1/tests/locked.move +++ b/contracts/liquidity_layer_v1/tests/locked.move @@ -1,23 +1,356 @@ #[test_only] /// Tests trading compatibility with locked NFTs module liquidity_layer_v1::test_orderbook_locked { + use std::option; + + use sui::package; + use sui::coin; + use sui::object::{Self, ID, UID}; + use sui::transfer; + use sui::sui::SUI; + use sui::kiosk::{Self, Kiosk}; + use sui::test_scenario::{Self, Scenario, ctx}; + use sui::transfer_policy::TransferPolicy; + + use ob_permissions::witness; + use ob_kiosk::ob_kiosk; + + use liquidity_layer_v1::orderbook::{Self, TradeInfo, Orderbook}; + + const CREATOR: address = @0xA1C03; + const BUYER: address = @0xA1C04; + const SELLER: address = @0xA1C05; + + struct Foo has key, store { + id: UID + } + + struct Witness has drop {} + struct TEST_ORDERBOOK_LOCKED has drop {} + + #[test_only] + /// Initializes `Orderbook` and `TransferPolicy` + fun init_ob(scenario: &mut Scenario) { + let publisher = package::test_claim(TEST_ORDERBOOK_LOCKED {}, ctx(scenario)); + let (tx_policy, policy_cap) = ob_request::transfer_request::init_policy(&publisher, ctx(scenario)); + + let delegated_witness = witness::from_witness(Witness {}); + + let ob = orderbook::new_unprotected(delegated_witness, &tx_policy, ctx(scenario)); + orderbook::share(ob); + + transfer::public_share_object(tx_policy); + transfer::public_transfer(publisher, CREATOR); + transfer::public_transfer(policy_cap, CREATOR); + + test_scenario::next_tx(scenario, CREATOR); + } + + #[test_only] + /// Initializes non-OB `Orderbook` and `TransferPolicy` + fun init_non_ob(scenario: &mut Scenario) { + let publisher = package::test_claim(TEST_ORDERBOOK_LOCKED {}, ctx(scenario)); + let (tx_policy, policy_cap) = sui::transfer_policy::new(&publisher, ctx(scenario)); + + orderbook::create_for_external(&tx_policy, ctx(scenario)); + + transfer::public_share_object(tx_policy); + transfer::public_transfer(publisher, CREATOR); + transfer::public_transfer(policy_cap, CREATOR); + + test_scenario::next_tx(scenario, CREATOR); + } + + #[test_only] + /// Generates a trade on the `Orderbook` that has to be finished + fun init_trade( + orderbook: &mut Orderbook, + seller_kiosk: &mut Kiosk, + buyer_kiosk: &mut Kiosk, + nft_id: ID, + scenario: &mut Scenario, + ): TradeInfo { + test_scenario::next_tx(scenario, SELLER); + + orderbook::create_ask( + orderbook, + seller_kiosk, + 1_000_000, + nft_id, + ctx(scenario), + ); + + test_scenario::next_tx(scenario, BUYER); + + let coin = coin::mint_for_testing(1_000_000, ctx(scenario)); + let trade_opt = orderbook::create_bid( + orderbook, + buyer_kiosk, + 1_000_000, + &mut coin, + ctx(scenario), + ); + let trade = option::destroy_some(trade_opt); + coin::burn_for_testing(coin); + + test_scenario::next_tx(scenario, CREATOR); + + trade + } + + fun init_kiosk_for_address(for: address, scenario: &mut Scenario): Kiosk { + test_scenario::next_tx(scenario, for); + let (buyer_kiosk, buyer_kiosk_cap) = kiosk::new(ctx(scenario)); + transfer::public_transfer(buyer_kiosk_cap, for); + + test_scenario::next_tx(scenario, CREATOR); + + buyer_kiosk + } + #[test] - fun test_transfer_to_unlocked() {} + fun test_transfer_locked_to_unlocked() { + let scenario = test_scenario::begin(CREATOR); + + // 1. Create prerequisites + init_ob(&mut scenario); + let tx_policy = test_scenario::take_shared>(&mut scenario); + let orderbook = test_scenario::take_shared>(&scenario); + let (buyer_kiosk, _) = ob_kiosk::new_for_address(BUYER, ctx(&mut scenario)); + let (seller_kiosk, _) = ob_kiosk::new_for_address(SELLER, ctx(&mut scenario)); + + // 2. Add NFT to seller kiosk + let nft = Foo { id: object::new(ctx(&mut scenario)) }; + let nft_id = object::id(&nft); + ob_kiosk::deposit_locked(&mut seller_kiosk, &tx_policy, nft, ctx(&mut scenario)); + + // 3. Perform trade on NFT and finish + let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); + + let request = orderbook::finish_trade( + &mut orderbook, + orderbook::trade_id(&trade), + &mut seller_kiosk, + &mut buyer_kiosk, + ctx(&mut scenario), + ); + ob_request::transfer_request::confirm(request, &tx_policy, ctx(&mut scenario)); + test_scenario::return_shared(tx_policy); + + // 4. Verify unlocked + assert!(!kiosk::is_locked(&buyer_kiosk, nft_id), 0); + + transfer::public_transfer(seller_kiosk, CREATOR); + transfer::public_transfer(buyer_kiosk, CREATOR); + test_scenario::return_shared(orderbook); + test_scenario::end(scenario); + } #[test] - fun test_transfer_to_unlocked_non_ob() {} + fun test_transfer_locked_to_unlocked_non_ob_buyer() { + let scenario = test_scenario::begin(CREATOR); + + // 1. Create prerequisites + init_non_ob(&mut scenario); + let tx_policy = test_scenario::take_shared>(&mut scenario); + let orderbook = test_scenario::take_shared>(&scenario); + let buyer_kiosk = init_kiosk_for_address(BUYER, &mut scenario); + let (seller_kiosk, _) = ob_kiosk::new_for_address(SELLER, ctx(&mut scenario)); + + // 2. Add NFT to seller kiosk + let nft = Foo { id: object::new(ctx(&mut scenario)) }; + let nft_id = object::id(&nft); + ob_kiosk::deposit_locked(&mut seller_kiosk, &tx_policy, nft, ctx(&mut scenario)); + + // 3. Perform trade on NFT and finish + let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); + + let request = orderbook::finish_trade( + &mut orderbook, + orderbook::trade_id(&trade), + &mut seller_kiosk, + &mut buyer_kiosk, + ctx(&mut scenario), + ); + ob_request::transfer_request::confirm(request, &tx_policy, ctx(&mut scenario)); + test_scenario::return_shared(tx_policy); + + // 4. Verify unlocked + assert!(!kiosk::is_locked(&buyer_kiosk, nft_id), 0); + + transfer::public_transfer(seller_kiosk, CREATOR); + transfer::public_transfer(buyer_kiosk, CREATOR); + test_scenario::return_shared(orderbook); + test_scenario::end(scenario); + } + + #[test] + fun test_transfer_locked_to_locked() { + let scenario = test_scenario::begin(CREATOR); + + // 1. Create prerequisites + init_ob(&mut scenario); + let tx_policy = test_scenario::take_shared>(&mut scenario); + let orderbook = test_scenario::take_shared>(&scenario); + let (buyer_kiosk, _) = ob_kiosk::new_for_address(BUYER, ctx(&mut scenario)); + let (seller_kiosk, _) = ob_kiosk::new_for_address(SELLER, ctx(&mut scenario)); + + // 2. Add NFT to seller kiosk + let nft = Foo { id: object::new(ctx(&mut scenario)) }; + let nft_id = object::id(&nft); + ob_kiosk::deposit_locked(&mut seller_kiosk, &tx_policy, nft, ctx(&mut scenario)); + + // 3. Perform trade on NFT and finish + let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); + + let request = orderbook::finish_trade_locked( + &mut orderbook, + orderbook::trade_id(&trade), + &mut seller_kiosk, + &mut buyer_kiosk, + &tx_policy, + ctx(&mut scenario), + ); + ob_request::transfer_request::confirm(request, &tx_policy, ctx(&mut scenario)); + test_scenario::return_shared(tx_policy); + + // 4. Verify locked + assert!(kiosk::is_locked(&buyer_kiosk, nft_id), 0); + + transfer::public_transfer(seller_kiosk, CREATOR); + transfer::public_transfer(buyer_kiosk, CREATOR); + test_scenario::return_shared(orderbook); + test_scenario::end(scenario); + } + + #[test] + fun test_transfer_locked_to_locked_non_ob_buyer() {} + + #[test] + fun test_transfer_unlocked_to_locked() { + let scenario = test_scenario::begin(CREATOR); + + // 1. Create prerequisites + init_ob(&mut scenario); + let tx_policy = test_scenario::take_shared>(&mut scenario); + let orderbook = test_scenario::take_shared>(&scenario); + let (buyer_kiosk, _) = ob_kiosk::new_for_address(BUYER, ctx(&mut scenario)); + let (seller_kiosk, _) = ob_kiosk::new_for_address(SELLER, ctx(&mut scenario)); + + // 2. Add NFT to seller kiosk + let nft = Foo { id: object::new(ctx(&mut scenario)) }; + let nft_id = object::id(&nft); + ob_kiosk::deposit(&mut seller_kiosk, nft, ctx(&mut scenario)); + + // 3. Perform trade on NFT and finish + let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); + + let request = orderbook::finish_trade_locked( + &mut orderbook, + orderbook::trade_id(&trade), + &mut seller_kiosk, + &mut buyer_kiosk, + &tx_policy, + ctx(&mut scenario), + ); + ob_request::transfer_request::confirm(request, &tx_policy, ctx(&mut scenario)); + test_scenario::return_shared(tx_policy); + + // 4. Verify locked + assert!(kiosk::is_locked(&buyer_kiosk, nft_id), 0); + + transfer::public_transfer(seller_kiosk, CREATOR); + transfer::public_transfer(buyer_kiosk, CREATOR); + test_scenario::return_shared(orderbook); + test_scenario::end(scenario); + } #[test] - fun test_transfer_to_locked() {} + fun test_transfer_unlocked_to_locked_non_ob_buyer() {} #[test] - fun test_transfer_to_locked_non_ob() {} + fun test_transfer_locked_to_inherit() { + let scenario = test_scenario::begin(CREATOR); + + // 1. Create prerequisites + init_ob(&mut scenario); + let tx_policy = test_scenario::take_shared>(&mut scenario); + let orderbook = test_scenario::take_shared>(&scenario); + let (buyer_kiosk, _) = ob_kiosk::new_for_address(BUYER, ctx(&mut scenario)); + let (seller_kiosk, _) = ob_kiosk::new_for_address(SELLER, ctx(&mut scenario)); + + // 2. Add NFT to seller kiosk + let nft = Foo { id: object::new(ctx(&mut scenario)) }; + let nft_id = object::id(&nft); + ob_kiosk::deposit_locked(&mut seller_kiosk, &tx_policy, nft, ctx(&mut scenario)); + + // 3. Perform trade on NFT and finish + let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); + + let request = orderbook::finish_trade_inherit( + &mut orderbook, + orderbook::trade_id(&trade), + &mut seller_kiosk, + &mut buyer_kiosk, + &tx_policy, + ctx(&mut scenario), + ); + ob_request::transfer_request::confirm(request, &tx_policy, ctx(&mut scenario)); + test_scenario::return_shared(tx_policy); + + // 4. Verify locked + assert!(kiosk::is_locked(&buyer_kiosk, nft_id), 0); + + transfer::public_transfer(seller_kiosk, CREATOR); + transfer::public_transfer(buyer_kiosk, CREATOR); + test_scenario::return_shared(orderbook); + test_scenario::end(scenario); + } #[test] - fun test_transfer_listed_non_ob() {} + fun test_transfer_locked_to_inherit_non_ob_buyer() {} + + #[test] + fun test_transfer_unlocked_to_inherit() { + let scenario = test_scenario::begin(CREATOR); + + // 1. Create prerequisites + init_ob(&mut scenario); + let tx_policy = test_scenario::take_shared>(&mut scenario); + let orderbook = test_scenario::take_shared>(&scenario); + let (buyer_kiosk, _) = ob_kiosk::new_for_address(BUYER, ctx(&mut scenario)); + let (seller_kiosk, _) = ob_kiosk::new_for_address(SELLER, ctx(&mut scenario)); + + // 2. Add NFT to seller kiosk + let nft = Foo { id: object::new(ctx(&mut scenario)) }; + let nft_id = object::id(&nft); + ob_kiosk::deposit(&mut seller_kiosk, nft, ctx(&mut scenario)); + + // 3. Perform trade on NFT and finish + let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); + + let request = orderbook::finish_trade_inherit( + &mut orderbook, + orderbook::trade_id(&trade), + &mut seller_kiosk, + &mut buyer_kiosk, + &tx_policy, + ctx(&mut scenario), + ); + ob_request::transfer_request::confirm(request, &tx_policy, ctx(&mut scenario)); + test_scenario::return_shared(tx_policy); + + // 4. Verify unlocked + assert!(!kiosk::is_locked(&buyer_kiosk, nft_id), 0); + + transfer::public_transfer(seller_kiosk, CREATOR); + transfer::public_transfer(buyer_kiosk, CREATOR); + test_scenario::return_shared(orderbook); + test_scenario::end(scenario); + } #[test] - fun test_transfer_exclusively_listed_non_ob() {} + fun test_transfer_unlocked_to_inherit_non_ob_buyer() {} #[test] fun test_request_payment() {} From cfd878cf4c7475d998b00df53b483e5af7ea7203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=81abor?= Date: Sat, 15 Jul 2023 18:31:46 +0200 Subject: [PATCH 5/9] Remove redundant endpoints --- contracts/kiosk/sources/kiosk/ob_kiosk.move | 100 +++--------------- .../sources/trading/orderbook.move | 66 +++--------- 2 files changed, 29 insertions(+), 137 deletions(-) diff --git a/contracts/kiosk/sources/kiosk/ob_kiosk.move b/contracts/kiosk/sources/kiosk/ob_kiosk.move index 2b71b2b3..0dd31669 100644 --- a/contracts/kiosk/sources/kiosk/ob_kiosk.move +++ b/contracts/kiosk/sources/kiosk/ob_kiosk.move @@ -537,7 +537,8 @@ module ob_kiosk::ob_kiosk { /// Transfer NFT out of Kiosk that has been previously delegated /// - /// NFT will not be locked in the target `Kiosk`. + /// Handles the case that NFT could be locked or not in the source `Kiosk`. + /// NFT will be locked in the target `Kiosk`. /// /// Requires that address of sender was previously passed to /// `auth_transfer`. @@ -565,6 +566,7 @@ module ob_kiosk::ob_kiosk { /// Transfer NFT out of Kiosk that has been previously delegated /// + /// Handles the case that NFT could be locked or not in the source `Kiosk`. /// NFT will be locked in the target `Kiosk`. /// /// Requires that address of sender was previously passed to @@ -622,71 +624,6 @@ module ob_kiosk::ob_kiosk { req } - /// Transfer locked NFT out of Kiosk that has been previously delegated to - /// a base Sui `Kiosk` - /// - /// The transferred NFT is immediately locked in the target `Kiosk`. - /// - /// Requires that `UID` of sender was previously passed to either - /// `auth_transfer` or `auth_exclusive_transfer`. - /// - /// Will always work if transaction sender is the `Kiosk` owner. - /// - /// #### Panics - /// - /// - Sender was not previously authorized for transfer or is not owner - /// - NFT does not exist - /// - Source is not an OriginByte `Kiosk` - public fun transfer_locked( - source: &mut Kiosk, - target: &mut Kiosk, - nft_id: ID, - entity_id: &UID, - paid: Coin, - transfer_policy: &sui::transfer_policy::TransferPolicy, - ctx: &mut TxContext, - ): TransferRequest { - assert_version_and_upgrade(ext(source)); - - let (nft, req) = transfer_locked_nft_( - source, nft_id, uid_to_address(entity_id), paid, ctx, - ); - deposit_locked(target, transfer_policy, nft, ctx); - req - } - - /// Transfer locked NFT out of Kiosk that has been previously delegated to - /// a base Sui `Kiosk` - /// - /// The transferred NFT is not locked in the target `Kiosk`. - /// - /// Requires that `UID` of sender was previously passed to either - /// `auth_transfer` or `auth_exclusive_transfer`. - /// - /// Will always work if transaction sender is the `Kiosk` owner. - /// - /// #### Panics - /// - /// - Sender was not previously authorized for transfer or is not owner - /// - NFT does not exist - /// - Source is not an OriginByte `Kiosk` - public fun transfer_unlocked( - source: &mut Kiosk, - target: &mut Kiosk, - nft_id: ID, - entity_id: &UID, - paid: Coin, - ctx: &mut TxContext, - ): TransferRequest { - assert_version_and_upgrade(ext(source)); - - let (nft, req) = transfer_locked_nft_( - source, nft_id, uid_to_address(entity_id), paid, ctx, - ); - deposit(target, nft, ctx); - req - } - /// Deprecated, use `transfer_locked` instead public fun transfer_locked_nft( _source: &mut Kiosk, @@ -991,28 +928,15 @@ module ob_kiosk::ob_kiosk { price: u64, ctx: &mut TxContext, ): (T, TransferRequest) { - let nft = remove_nft(self, nft_id, originator, ctx); - (nft, transfer_request::new(nft_id, originator, object::id(self), price, ctx)) - } - - /// Initializes a transfer transaction for locked NFT - /// - /// #### Panics - /// - /// - Originator is not authorized to withdraw and transaction sender is - /// not owner. - /// - NFT does not exist - /// - NFT is not locked - fun transfer_locked_nft_( - self: &mut Kiosk, - nft_id: ID, - originator: address, - paid: Coin, - ctx: &mut TxContext, - ): (T, TransferRequest) { - // TODO: Merge with `transfer_nft_` - let (nft, req) = remove_locked_nft(self, nft_id, originator, paid, ctx); - (nft, transfer_request::from_sui(req, nft_id, originator, ctx)) + if (kiosk::is_locked(self, nft_id)) { + let (nft, req) = remove_locked_nft( + self, nft_id, originator, coin::zero(ctx), ctx, + ); + (nft, transfer_request::from_sui(req, nft_id, originator, ctx)) + } else { + let nft = remove_nft(self, nft_id, originator, ctx); + (nft, transfer_request::new(nft_id, originator, object::id(self), price, ctx)) + } } /// Initializes a withdrawal transaction diff --git a/contracts/liquidity_layer_v1/sources/trading/orderbook.move b/contracts/liquidity_layer_v1/sources/trading/orderbook.move index cc11e9ea..9996da59 100644 --- a/contracts/liquidity_layer_v1/sources/trading/orderbook.move +++ b/contracts/liquidity_layer_v1/sources/trading/orderbook.move @@ -758,24 +758,12 @@ module liquidity_layer_v1::orderbook { assert_version_and_upgrade(book); let trade = finalize_seller_side(book, trade_id, seller_kiosk); + let price = balance::value(&trade.paid); let nft_id = trade.nft_id; - let transfer_req = if (kiosk::is_locked(seller_kiosk, nft_id)) { - // This will cause the NFT to be transfered without locking - ob_kiosk::transfer_unlocked( - seller_kiosk, - buyer_kiosk, - nft_id, - &book.id, - coin::zero(ctx), - ctx, - ) - } else { - let price = balance::value(&trade.paid); - ob_kiosk::transfer_delegated( - seller_kiosk, buyer_kiosk, nft_id, &book.id, price, ctx, - ) - }; + let transfer_req = ob_kiosk::transfer_delegated( + seller_kiosk, buyer_kiosk, nft_id, &book.id, price, ctx, + ); finalize_buyer_side(&mut transfer_req, trade, buyer_kiosk, ctx); @@ -829,31 +817,18 @@ module liquidity_layer_v1::orderbook { assert_version_and_upgrade(book); let trade = finalize_seller_side(book, trade_id, seller_kiosk); + let price = balance::value(&trade.paid); let nft_id = trade.nft_id; - let transfer_req = if (kiosk::is_locked(seller_kiosk, nft_id)) { - // This will cause the NFT to be transfered and locked - ob_kiosk::transfer_locked( - seller_kiosk, - buyer_kiosk, - nft_id, - &book.id, - coin::zero(ctx), - transfer_policy, - ctx, - ) - } else { - let price = balance::value(&trade.paid); - ob_kiosk::transfer_delegated_locked( - seller_kiosk, - buyer_kiosk, - nft_id, - &book.id, - price, - transfer_policy, - ctx, - ) - }; + let transfer_req = ob_kiosk::transfer_delegated_locked( + seller_kiosk, + buyer_kiosk, + nft_id, + &book.id, + price, + transfer_policy, + ctx, + ); finalize_buyer_side(&mut transfer_req, trade, buyer_kiosk, ctx); @@ -890,21 +865,14 @@ module liquidity_layer_v1::orderbook { assert_version_and_upgrade(book); let trade = finalize_seller_side(book, trade_id, seller_kiosk); + let price = balance::value(&trade.paid); let nft_id = trade.nft_id; let transfer_req = if (kiosk::is_locked(seller_kiosk, nft_id)) { - // This will cause the NFT to be transfered and locked - ob_kiosk::transfer_locked( - seller_kiosk, - buyer_kiosk, - nft_id, - &book.id, - coin::zero(ctx), - transfer_policy, - ctx, + ob_kiosk::transfer_delegated_locked( + seller_kiosk, buyer_kiosk, nft_id, &book.id, price, transfer_policy, ctx, ) } else { - let price = balance::value(&trade.paid); ob_kiosk::transfer_delegated( seller_kiosk, buyer_kiosk, nft_id, &book.id, price, ctx, ) From 8e192b7171beb914799ba29ec899ccc47eb6bbcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=81abor?= Date: Sat, 15 Jul 2023 18:51:43 +0200 Subject: [PATCH 6/9] Finalize tests --- .../sources/trading/orderbook.move | 102 ++++++++++- .../liquidity_layer_v1/tests/locked.move | 167 ++++++++++++++++-- 2 files changed, 247 insertions(+), 22 deletions(-) diff --git a/contracts/liquidity_layer_v1/sources/trading/orderbook.move b/contracts/liquidity_layer_v1/sources/trading/orderbook.move index 9996da59..5a4c5ed1 100644 --- a/contracts/liquidity_layer_v1/sources/trading/orderbook.move +++ b/contracts/liquidity_layer_v1/sources/trading/orderbook.move @@ -746,6 +746,7 @@ module liquidity_layer_v1::orderbook { /// /// #### Panics /// + /// - Trade `ID` does not exist /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless /// buyer is the transaction sender. public fun finish_trade( @@ -770,6 +771,16 @@ module liquidity_layer_v1::orderbook { transfer_req } + /// Optimistic equivalent of `finish_trade` + /// + /// Executes a trade after orders have been matched but will not panic if + /// `Kiosks` do not match the trade ID. + /// + /// #### Panics + /// + /// - Trade `ID` does not exist + /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless + /// buyer is the transaction sender. public fun finish_trade_if_kiosks_match( book: &mut Orderbook, trade_id: ID, @@ -777,13 +788,16 @@ module liquidity_layer_v1::orderbook { buyer_kiosk: &mut Kiosk, ctx: &mut TxContext ): Option> { - assert_version_and_upgrade(book); + // Version asserted by `finish_trade` - let t = trade(book, trade_id); - let kiosks_match = &t.seller_kiosk == &object::id(seller_kiosk) && &t.buyer_kiosk == &object::id(buyer_kiosk); + let trade = trade(book, trade_id); + let kiosks_match = &trade.seller_kiosk == &object::id(seller_kiosk) + && &trade.buyer_kiosk == &object::id(buyer_kiosk); if (kiosks_match) { - option::some(finish_trade(book, trade_id, seller_kiosk, buyer_kiosk, ctx)) + option::some( + finish_trade(book, trade_id, seller_kiosk, buyer_kiosk, ctx) + ) } else { option::none() } @@ -835,6 +849,46 @@ module liquidity_layer_v1::orderbook { transfer_req } + /// Optimistic equivalent of `finish_trade_locked` + /// + /// Executes a trade after orders have been matched but will not panic if + /// `Kiosks` do not match the trade ID. + /// + /// #### Panics + /// + /// - Trade `ID` does not exist + /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless + /// buyer is the transaction sender. + public fun finish_trade_locked_if_kiosks_match( + book: &mut Orderbook, + trade_id: ID, + seller_kiosk: &mut Kiosk, + buyer_kiosk: &mut Kiosk, + transfer_policy: &sui::transfer_policy::TransferPolicy, + ctx: &mut TxContext + ): Option> { + // Version asserted by `finish_trade_locked` + + let trade = trade(book, trade_id); + let kiosks_match = &trade.seller_kiosk == &object::id(seller_kiosk) + && &trade.buyer_kiosk == &object::id(buyer_kiosk); + + if (kiosks_match) { + option::some( + finish_trade_locked( + book, + trade_id, + seller_kiosk, + buyer_kiosk, + transfer_policy, + ctx + ) + ) + } else { + option::none() + } + } + /// Executes a trade after orders have been matched /// /// Inherits the NFTs locked state from the source `Kiosk` to the target. @@ -883,6 +937,46 @@ module liquidity_layer_v1::orderbook { transfer_req } + /// Optimistic equivalent of `finish_trade_inherit` + /// + /// Executes a trade after orders have been matched but will not panic if + /// `Kiosks` do not match the trade ID. + /// + /// #### Panics + /// + /// - Trade `ID` does not exist + /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless + /// buyer is the transaction sender. + public fun finish_trade_inherit_if_kiosks_match( + book: &mut Orderbook, + trade_id: ID, + seller_kiosk: &mut Kiosk, + buyer_kiosk: &mut Kiosk, + transfer_policy: &sui::transfer_policy::TransferPolicy, + ctx: &mut TxContext + ): Option> { + // Version asserted by `finish_trade_inherit` + + let trade = trade(book, trade_id); + let kiosks_match = &trade.seller_kiosk == &object::id(seller_kiosk) + && &trade.buyer_kiosk == &object::id(buyer_kiosk); + + if (kiosks_match) { + option::some( + finish_trade_inherit( + book, + trade_id, + seller_kiosk, + buyer_kiosk, + transfer_policy, + ctx + ) + ) + } else { + option::none() + } + } + /// Finalizes trade on the seller side by resolving `TradeIntermediate` /// /// #### Panics diff --git a/contracts/liquidity_layer_v1/tests/locked.move b/contracts/liquidity_layer_v1/tests/locked.move index da532334..7ae92481 100644 --- a/contracts/liquidity_layer_v1/tests/locked.move +++ b/contracts/liquidity_layer_v1/tests/locked.move @@ -98,16 +98,6 @@ module liquidity_layer_v1::test_orderbook_locked { trade } - fun init_kiosk_for_address(for: address, scenario: &mut Scenario): Kiosk { - test_scenario::next_tx(scenario, for); - let (buyer_kiosk, buyer_kiosk_cap) = kiosk::new(ctx(scenario)); - transfer::public_transfer(buyer_kiosk_cap, for); - - test_scenario::next_tx(scenario, CREATOR); - - buyer_kiosk - } - #[test] fun test_transfer_locked_to_unlocked() { let scenario = test_scenario::begin(CREATOR); @@ -147,14 +137,14 @@ module liquidity_layer_v1::test_orderbook_locked { } #[test] - fun test_transfer_locked_to_unlocked_non_ob_buyer() { + fun test_transfer_locked_to_unlocked_non_ob_policy() { let scenario = test_scenario::begin(CREATOR); // 1. Create prerequisites init_non_ob(&mut scenario); let tx_policy = test_scenario::take_shared>(&mut scenario); let orderbook = test_scenario::take_shared>(&scenario); - let buyer_kiosk = init_kiosk_for_address(BUYER, &mut scenario); + let (buyer_kiosk, _) = ob_kiosk::new_for_address(BUYER, ctx(&mut scenario)); let (seller_kiosk, _) = ob_kiosk::new_for_address(SELLER, ctx(&mut scenario)); // 2. Add NFT to seller kiosk @@ -224,7 +214,43 @@ module liquidity_layer_v1::test_orderbook_locked { } #[test] - fun test_transfer_locked_to_locked_non_ob_buyer() {} + fun test_transfer_locked_to_locked_non_ob_policy() { + let scenario = test_scenario::begin(CREATOR); + + // 1. Create prerequisites + init_non_ob(&mut scenario); + let tx_policy = test_scenario::take_shared>(&mut scenario); + let orderbook = test_scenario::take_shared>(&scenario); + let (buyer_kiosk, _) = ob_kiosk::new_for_address(BUYER, ctx(&mut scenario)); + let (seller_kiosk, _) = ob_kiosk::new_for_address(SELLER, ctx(&mut scenario)); + + // 2. Add NFT to seller kiosk + let nft = Foo { id: object::new(ctx(&mut scenario)) }; + let nft_id = object::id(&nft); + ob_kiosk::deposit_locked(&mut seller_kiosk, &tx_policy, nft, ctx(&mut scenario)); + + // 3. Perform trade on NFT and finish + let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); + + let request = orderbook::finish_trade_locked( + &mut orderbook, + orderbook::trade_id(&trade), + &mut seller_kiosk, + &mut buyer_kiosk, + &tx_policy, + ctx(&mut scenario), + ); + ob_request::transfer_request::confirm(request, &tx_policy, ctx(&mut scenario)); + test_scenario::return_shared(tx_policy); + + // 4. Verify locked + assert!(kiosk::is_locked(&buyer_kiosk, nft_id), 0); + + transfer::public_transfer(seller_kiosk, CREATOR); + transfer::public_transfer(buyer_kiosk, CREATOR); + test_scenario::return_shared(orderbook); + test_scenario::end(scenario); + } #[test] fun test_transfer_unlocked_to_locked() { @@ -266,7 +292,43 @@ module liquidity_layer_v1::test_orderbook_locked { } #[test] - fun test_transfer_unlocked_to_locked_non_ob_buyer() {} + fun test_transfer_unlocked_to_locked_non_ob_policy() { + let scenario = test_scenario::begin(CREATOR); + + // 1. Create prerequisites + init_non_ob(&mut scenario); + let tx_policy = test_scenario::take_shared>(&mut scenario); + let orderbook = test_scenario::take_shared>(&scenario); + let (buyer_kiosk, _) = ob_kiosk::new_for_address(BUYER, ctx(&mut scenario)); + let (seller_kiosk, _) = ob_kiosk::new_for_address(SELLER, ctx(&mut scenario)); + + // 2. Add NFT to seller kiosk + let nft = Foo { id: object::new(ctx(&mut scenario)) }; + let nft_id = object::id(&nft); + ob_kiosk::deposit(&mut seller_kiosk, nft, ctx(&mut scenario)); + + // 3. Perform trade on NFT and finish + let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); + + let request = orderbook::finish_trade_locked( + &mut orderbook, + orderbook::trade_id(&trade), + &mut seller_kiosk, + &mut buyer_kiosk, + &tx_policy, + ctx(&mut scenario), + ); + ob_request::transfer_request::confirm(request, &tx_policy, ctx(&mut scenario)); + test_scenario::return_shared(tx_policy); + + // 4. Verify locked + assert!(kiosk::is_locked(&buyer_kiosk, nft_id), 0); + + transfer::public_transfer(seller_kiosk, CREATOR); + transfer::public_transfer(buyer_kiosk, CREATOR); + test_scenario::return_shared(orderbook); + test_scenario::end(scenario); + } #[test] fun test_transfer_locked_to_inherit() { @@ -308,7 +370,43 @@ module liquidity_layer_v1::test_orderbook_locked { } #[test] - fun test_transfer_locked_to_inherit_non_ob_buyer() {} + fun test_transfer_locked_to_inherit_non_ob_policy() { + let scenario = test_scenario::begin(CREATOR); + + // 1. Create prerequisites + init_non_ob(&mut scenario); + let tx_policy = test_scenario::take_shared>(&mut scenario); + let orderbook = test_scenario::take_shared>(&scenario); + let (buyer_kiosk, _) = ob_kiosk::new_for_address(BUYER, ctx(&mut scenario)); + let (seller_kiosk, _) = ob_kiosk::new_for_address(SELLER, ctx(&mut scenario)); + + // 2. Add NFT to seller kiosk + let nft = Foo { id: object::new(ctx(&mut scenario)) }; + let nft_id = object::id(&nft); + ob_kiosk::deposit_locked(&mut seller_kiosk, &tx_policy, nft, ctx(&mut scenario)); + + // 3. Perform trade on NFT and finish + let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); + + let request = orderbook::finish_trade_inherit( + &mut orderbook, + orderbook::trade_id(&trade), + &mut seller_kiosk, + &mut buyer_kiosk, + &tx_policy, + ctx(&mut scenario), + ); + ob_request::transfer_request::confirm(request, &tx_policy, ctx(&mut scenario)); + test_scenario::return_shared(tx_policy); + + // 4. Verify locked + assert!(kiosk::is_locked(&buyer_kiosk, nft_id), 0); + + transfer::public_transfer(seller_kiosk, CREATOR); + transfer::public_transfer(buyer_kiosk, CREATOR); + test_scenario::return_shared(orderbook); + test_scenario::end(scenario); + } #[test] fun test_transfer_unlocked_to_inherit() { @@ -350,8 +448,41 @@ module liquidity_layer_v1::test_orderbook_locked { } #[test] - fun test_transfer_unlocked_to_inherit_non_ob_buyer() {} + fun test_transfer_unlocked_to_inherit_non_ob_policy() { + let scenario = test_scenario::begin(CREATOR); - #[test] - fun test_request_payment() {} + // 1. Create prerequisites + init_non_ob(&mut scenario); + let tx_policy = test_scenario::take_shared>(&mut scenario); + let orderbook = test_scenario::take_shared>(&scenario); + let (buyer_kiosk, _) = ob_kiosk::new_for_address(BUYER, ctx(&mut scenario)); + let (seller_kiosk, _) = ob_kiosk::new_for_address(SELLER, ctx(&mut scenario)); + + // 2. Add NFT to seller kiosk + let nft = Foo { id: object::new(ctx(&mut scenario)) }; + let nft_id = object::id(&nft); + ob_kiosk::deposit(&mut seller_kiosk, nft, ctx(&mut scenario)); + + // 3. Perform trade on NFT and finish + let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); + + let request = orderbook::finish_trade_inherit( + &mut orderbook, + orderbook::trade_id(&trade), + &mut seller_kiosk, + &mut buyer_kiosk, + &tx_policy, + ctx(&mut scenario), + ); + ob_request::transfer_request::confirm(request, &tx_policy, ctx(&mut scenario)); + test_scenario::return_shared(tx_policy); + + // 4. Verify unlocked + assert!(!kiosk::is_locked(&buyer_kiosk, nft_id), 0); + + transfer::public_transfer(seller_kiosk, CREATOR); + transfer::public_transfer(buyer_kiosk, CREATOR); + test_scenario::return_shared(orderbook); + test_scenario::end(scenario); + } } \ No newline at end of file From 985254b7284786839d7fd545a007f8d449877eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=81abor?= Date: Mon, 17 Jul 2023 21:41:51 +0200 Subject: [PATCH 7/9] Respect Sui royalties --- contracts/kiosk/sources/kiosk/ob_kiosk.move | 107 +++++- .../sources/trading/orderbook.move | 344 +++++++++++++++++- .../liquidity_layer_v1/tests/locked.move | 164 ++++++++- 3 files changed, 597 insertions(+), 18 deletions(-) diff --git a/contracts/kiosk/sources/kiosk/ob_kiosk.move b/contracts/kiosk/sources/kiosk/ob_kiosk.move index 0dd31669..ea7a65d2 100644 --- a/contracts/kiosk/sources/kiosk/ob_kiosk.move +++ b/contracts/kiosk/sources/kiosk/ob_kiosk.move @@ -29,7 +29,7 @@ /// - Permissionless `Kiosk` needs to signer, apps don't have to wrap both /// the `KioskOwnerCap` and the `Kiosk` in a smart contract. module ob_kiosk::ob_kiosk { - use std::option::Option; + use std::option::{Self, Option}; use std::string::utf8; use std::vector; use std::type_name::{Self, TypeName}; @@ -535,6 +535,18 @@ module ob_kiosk::ob_kiosk { (target_kiosk_id, target_token) } + /// Deprecated, use `transfer_delegated_unlocked` + public fun transfer_delegated( + _source: &mut Kiosk, + _target: &mut Kiosk, + _nft_id: ID, + _entity_id: &UID, + _price: u64, + _ctx: &mut TxContext, + ): TransferRequest { + abort(EDeprecatedApi) + } + /// Transfer NFT out of Kiosk that has been previously delegated /// /// Handles the case that NFT could be locked or not in the source `Kiosk`. @@ -543,57 +555,84 @@ module ob_kiosk::ob_kiosk { /// Requires that address of sender was previously passed to /// `auth_transfer`. /// + /// `paid` argument is used to pass the amount on which royalties must be + /// paid on the base Sui transfer request, whilst, `price` is used on the + /// OB request. Will panic if `paid` is provided for a non-locked NFT. + /// /// #### Panics /// /// - Entity `UID` was not previously authorized for transfer /// - NFT does not exist /// - Target `Kiosk` deposit conditions were not met, see `deposit` method /// - Source or target `Kiosk` are not OriginByte kiosks - public fun transfer_delegated( + /// - Panics if `Coin` is provided for an NFT that is not + /// locked + public fun transfer_delegated_unlocked( source: &mut Kiosk, target: &mut Kiosk, nft_id: ID, entity_id: &UID, price: u64, + paid: Option>, ctx: &mut TxContext, ): TransferRequest { assert_version_and_upgrade(ext(source)); - let (nft, req) = transfer_nft_(source, nft_id, uid_to_address(entity_id), price, ctx); + let (nft, req) = transfer_nft_(source, nft_id, uid_to_address(entity_id), price, paid, ctx); deposit(target, nft, ctx); req } /// Transfer NFT out of Kiosk that has been previously delegated /// + /// NFT will be locked in the target `Kiosk`. + /// /// Handles the case that NFT could be locked or not in the source `Kiosk`. /// NFT will be locked in the target `Kiosk`. /// /// Requires that address of sender was previously passed to /// `auth_transfer`. /// + /// `paid` argument is used to pass the amount on which royalties must be + /// paid on the base Sui transfer request, whilst, `price` is used on the + /// OB request. Will panic if `paid` is provided for a non-locked NFT. + /// /// #### Panics /// /// - Entity `UID` was not previously authorized for transfer /// - NFT does not exist /// - Target `Kiosk` deposit conditions were not met, see `deposit` method /// - Source or target `Kiosk` are not OriginByte kiosks + /// - Panics if `Coin` is provided for an NFT that is not + /// locked public fun transfer_delegated_locked( source: &mut Kiosk, target: &mut Kiosk, nft_id: ID, entity_id: &UID, price: u64, + paid: Option>, transfer_policy: &sui::transfer_policy::TransferPolicy, ctx: &mut TxContext, ): TransferRequest { assert_version_and_upgrade(ext(source)); - let (nft, req) = transfer_nft_(source, nft_id, uid_to_address(entity_id), price, ctx); + let (nft, req) = transfer_nft_(source, nft_id, uid_to_address(entity_id), price, paid, ctx); deposit_locked(target, transfer_policy, nft, ctx); req } + /// Deprecated, use `transfer_signed_unlocked` + public fun transfer_signed( + _source: &mut Kiosk, + _target: &mut Kiosk, + _nft_id: ID, + _price: u64, + _ctx: &mut TxContext, + ): TransferRequest { + abort(EDeprecatedApi) + } + /// Transfer NFT out of Kiosk that has been previously delegated /// /// Requires that address of sender was previously passed to @@ -607,11 +646,49 @@ module ob_kiosk::ob_kiosk { /// - NFT does not exist /// - Target `Kiosk` deposit conditions were not met, see `deposit` method /// - Source or target `Kiosk` are not OriginByte kiosks - public fun transfer_signed( + /// - Panics if `Coin` is provided for an NFT that is not + /// locked + public fun transfer_signed_unlocked( + source: &mut Kiosk, + target: &mut Kiosk, + nft_id: ID, + price: u64, + paid: Option>, + ctx: &mut TxContext, + ): TransferRequest { + assert_version_and_upgrade(ext(source)); + // Exclusive transfers need to be settled via `transfer_delegated` + // otherwise it's possible to create dangling locks + assert_not_exclusively_listed(source, nft_id); + + let (nft, req) = transfer_nft_(source, nft_id, sender(ctx), price, paid, ctx); + deposit(target, nft, ctx); + req + } + + /// Transfer NFT out of Kiosk that has been previously delegated + /// + /// NFT will be locked in the target `Kiosk` + /// + /// Requires that address of sender was previously passed to + /// `auth_transfer` or transaction sender is `Kiosk` owner. + /// + /// Will always work if transaction sender is the `Kiosk` owner. + /// + /// #### Panics + /// + /// - Sender was not previously authorized for transfer or is not owner + /// - NFT does not exist + /// - Target `Kiosk` deposit conditions were not met, see `deposit` method + /// - Source or target `Kiosk` are not OriginByte kiosks + /// - Panics if `Coin` is provided for an NFT that is not + /// locked + public fun transfer_signed_locked( source: &mut Kiosk, target: &mut Kiosk, nft_id: ID, price: u64, + paid: Option>, ctx: &mut TxContext, ): TransferRequest { assert_version_and_upgrade(ext(source)); @@ -619,7 +696,7 @@ module ob_kiosk::ob_kiosk { // otherwise it's possible to create dangling locks assert_not_exclusively_listed(source, nft_id); - let (nft, req) = transfer_nft_(source, nft_id, sender(ctx), price, ctx); + let (nft, req) = transfer_nft_(source, nft_id, sender(ctx), price, paid, ctx); deposit(target, nft, ctx); req } @@ -920,20 +997,30 @@ module ob_kiosk::ob_kiosk { /// - Originator is not authorized to withdraw and transaction sender is /// not owner. /// - NFT does not exist - /// - NFT is locked + /// - Panics if `Coin` is provided for an NFT that is not + /// locked fun transfer_nft_( self: &mut Kiosk, nft_id: ID, originator: address, price: u64, + paid: Option>, ctx: &mut TxContext, ): (T, TransferRequest) { if (kiosk::is_locked(self, nft_id)) { - let (nft, req) = remove_locked_nft( - self, nft_id, originator, coin::zero(ctx), ctx, - ); + let paid = if (option::is_some(&paid)) { + option::destroy_some(paid) + } else { + option::destroy_none(paid); + coin::zero(ctx) + }; + + let (nft, req) = + remove_locked_nft(self, nft_id, originator, paid, ctx); (nft, transfer_request::from_sui(req, nft_id, originator, ctx)) } else { + option::destroy_none(paid); + let nft = remove_nft(self, nft_id, originator, ctx); (nft, transfer_request::new(nft_id, originator, object::id(self), price, ctx)) } diff --git a/contracts/liquidity_layer_v1/sources/trading/orderbook.move b/contracts/liquidity_layer_v1/sources/trading/orderbook.move index 5a4c5ed1..ae6a7bb1 100644 --- a/contracts/liquidity_layer_v1/sources/trading/orderbook.move +++ b/contracts/liquidity_layer_v1/sources/trading/orderbook.move @@ -29,6 +29,7 @@ module liquidity_layer_v1::orderbook { use std::type_name; use std::vector; + use sui::sui::SUI; use sui::event; use sui::package::{Self, Publisher}; use sui::transfer_policy::TransferPolicy; @@ -129,6 +130,9 @@ module liquidity_layer_v1::orderbook { /// Tried to resolve a `TradeIntermediate` field when one did not exist const EUndefinedTradeIntermediate: u64 = 18; + /// Tried to resolve `SUI` trade using generic endpoint + const EIncorrectEndpoint: u64 = 19; + // === Structs === /// Add this witness type to allowlists via @@ -746,6 +750,7 @@ module liquidity_layer_v1::orderbook { /// /// #### Panics /// + /// - Tried to trade locked NFT with SUI /// - Trade `ID` does not exist /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless /// buyer is the transaction sender. @@ -762,8 +767,15 @@ module liquidity_layer_v1::orderbook { let price = balance::value(&trade.paid); let nft_id = trade.nft_id; - let transfer_req = ob_kiosk::transfer_delegated( - seller_kiosk, buyer_kiosk, nft_id, &book.id, price, ctx, + // Do not allow trading `SUI` using generic function + assert!( + kiosk::is_locked(seller_kiosk, nft_id) && + std::type_name::get() == std::type_name::get(), + EIncorrectEndpoint + ); + + let transfer_req = ob_kiosk::transfer_delegated_unlocked( + seller_kiosk, buyer_kiosk, nft_id, &book.id, price, option::none(), ctx, ); finalize_buyer_side(&mut transfer_req, trade, buyer_kiosk, ctx); @@ -778,6 +790,7 @@ module liquidity_layer_v1::orderbook { /// /// #### Panics /// + /// - Tried to trade locked NFT with SUI /// - Trade `ID` does not exist /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless /// buyer is the transaction sender. @@ -818,6 +831,8 @@ module liquidity_layer_v1::orderbook { /// /// #### Panics /// + /// - Tried to trade locked NFT with SUI + /// - Trade `ID` does not exist /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless /// buyer is the transaction sender. public fun finish_trade_locked( @@ -834,12 +849,20 @@ module liquidity_layer_v1::orderbook { let price = balance::value(&trade.paid); let nft_id = trade.nft_id; + // Do not allow trading `SUI` using generic function + assert!( + kiosk::is_locked(seller_kiosk, nft_id) && + std::type_name::get() == std::type_name::get(), + EIncorrectEndpoint + ); + let transfer_req = ob_kiosk::transfer_delegated_locked( seller_kiosk, buyer_kiosk, nft_id, &book.id, price, + option::none(), transfer_policy, ctx, ); @@ -856,6 +879,7 @@ module liquidity_layer_v1::orderbook { /// /// #### Panics /// + /// - Tried to trade locked NFT with SUI /// - Trade `ID` does not exist /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless /// buyer is the transaction sender. @@ -906,6 +930,8 @@ module liquidity_layer_v1::orderbook { /// /// #### Panics /// + /// - Tried to trade locked NFT with SUI + /// - Trade `ID` does not exist /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless /// buyer is the transaction sender. public fun finish_trade_inherit( @@ -922,13 +948,33 @@ module liquidity_layer_v1::orderbook { let price = balance::value(&trade.paid); let nft_id = trade.nft_id; + // Do not allow trading `SUI` using generic function + assert!( + kiosk::is_locked(seller_kiosk, nft_id) && + std::type_name::get() == std::type_name::get(), + EIncorrectEndpoint + ); + let transfer_req = if (kiosk::is_locked(seller_kiosk, nft_id)) { ob_kiosk::transfer_delegated_locked( - seller_kiosk, buyer_kiosk, nft_id, &book.id, price, transfer_policy, ctx, + seller_kiosk, + buyer_kiosk, + nft_id, + &book.id, + price, + option::none(), + transfer_policy, + ctx, ) } else { - ob_kiosk::transfer_delegated( - seller_kiosk, buyer_kiosk, nft_id, &book.id, price, ctx, + ob_kiosk::transfer_delegated_unlocked( + seller_kiosk, + buyer_kiosk, + nft_id, + &book.id, + price, + option::none(), + ctx, ) }; @@ -977,6 +1023,291 @@ module liquidity_layer_v1::orderbook { } } + /// Equivalent to `finish_trade` but respects SUI royalties + /// + /// #### Panics + /// + /// - Trade `ID` does not exist + /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless + /// buyer is the transaction sender. + public fun finish_sui_trade( + book: &mut Orderbook, + trade_id: ID, + seller_kiosk: &mut Kiosk, + buyer_kiosk: &mut Kiosk, + ctx: &mut TxContext, + ): TransferRequest { + assert_version_and_upgrade(book); + + let trade = finalize_seller_side(book, trade_id, seller_kiosk); + let price = balance::value(&trade.paid); + let nft_id = trade.nft_id; + + let transfer_req = if (kiosk::is_locked(seller_kiosk, nft_id)) { + let paid = coin::take(&mut trade.paid, price, ctx); + ob_kiosk::transfer_delegated_unlocked( + seller_kiosk, + buyer_kiosk, + nft_id, + &book.id, + 0, + option::some(paid), + ctx, + ) + } else { + ob_kiosk::transfer_delegated_unlocked( + seller_kiosk, + buyer_kiosk, + nft_id, + &book.id, + price, + option::none(), + ctx, + ) + }; + + finalize_buyer_side(&mut transfer_req, trade, buyer_kiosk, ctx); + + transfer_req + } + + /// Optimistic equivalent of `finish_sui_trade` + /// + /// Executes a trade after orders have been matched but will not panic if + /// `Kiosks` do not match the trade ID. + /// + /// #### Panics + /// + /// - Trade `ID` does not exist + /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless + /// buyer is the transaction sender. + public fun finish_sui_trade_if_kiosks_match( + book: &mut Orderbook, + trade_id: ID, + seller_kiosk: &mut Kiosk, + buyer_kiosk: &mut Kiosk, + ctx: &mut TxContext + ): Option> { + // Version asserted by `finish_trade` + + let trade = trade(book, trade_id); + let kiosks_match = &trade.seller_kiosk == &object::id(seller_kiosk) + && &trade.buyer_kiosk == &object::id(buyer_kiosk); + + if (kiosks_match) { + option::some( + finish_sui_trade(book, trade_id, seller_kiosk, buyer_kiosk, ctx) + ) + } else { + option::none() + } + } + + /// Executes a trade after orders have been matched + /// + /// NFT will be deposited in target `Kiosk` and immediately locked. + /// + /// A separate trade execution step is necessary as we don't know the + /// target `Kiosk` upfront as the best bid or ask can change at any time. + /// + /// To resolve this, `Orderbook` creates a `TradeIntermediate` dynamic + /// field which can be permissionlessly resolved via this endpoint. + /// + /// See the documentation for `nft_protocol::transfer_request` to understand + /// how to deal with the returned [`TransferRequest`] type. + /// + /// #### Panics + /// + /// - Trade `ID` does not exist + /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless + /// buyer is the transaction sender. + public fun finish_sui_trade_locked( + book: &mut Orderbook, + trade_id: ID, + seller_kiosk: &mut Kiosk, + buyer_kiosk: &mut Kiosk, + transfer_policy: &sui::transfer_policy::TransferPolicy, + ctx: &mut TxContext, + ): TransferRequest { + assert_version_and_upgrade(book); + + let trade = finalize_seller_side(book, trade_id, seller_kiosk); + let price = balance::value(&trade.paid); + let nft_id = trade.nft_id; + + let transfer_req = if (kiosk::is_locked(seller_kiosk, nft_id)) { + let paid = coin::take(&mut trade.paid, price, ctx); + ob_kiosk::transfer_delegated_locked( + seller_kiosk, + buyer_kiosk, + nft_id, + &book.id, + 0, + option::some(paid), + transfer_policy, + ctx, + ) + } else { + ob_kiosk::transfer_delegated_locked( + seller_kiosk, + buyer_kiosk, + nft_id, + &book.id, + price, + option::none(), + transfer_policy, + ctx, + ) + }; + + finalize_buyer_side(&mut transfer_req, trade, buyer_kiosk, ctx); + + transfer_req + } + + /// Optimistic equivalent of `finish_sui_trade_locked` + /// + /// Executes a trade after orders have been matched but will not panic if + /// `Kiosks` do not match the trade ID. + /// + /// #### Panics + /// + /// - Trade `ID` does not exist + /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless + /// buyer is the transaction sender. + public fun finish_sui_trade_locked_if_kiosks_match( + book: &mut Orderbook, + trade_id: ID, + seller_kiosk: &mut Kiosk, + buyer_kiosk: &mut Kiosk, + transfer_policy: &sui::transfer_policy::TransferPolicy, + ctx: &mut TxContext + ): Option> { + // Version asserted by `finish_trade_locked` + + let trade = trade(book, trade_id); + let kiosks_match = &trade.seller_kiosk == &object::id(seller_kiosk) + && &trade.buyer_kiosk == &object::id(buyer_kiosk); + + if (kiosks_match) { + option::some( + finish_sui_trade_locked( + book, + trade_id, + seller_kiosk, + buyer_kiosk, + transfer_policy, + ctx + ) + ) + } else { + option::none() + } + } + + /// Executes a trade after orders have been matched + /// + /// Inherits the NFTs locked state from the source `Kiosk` to the target. + /// - If NFT was locked in source it will be locked in target + /// - If NFT was unlocked in source it will be locked in target + /// + /// A separate trade execution step is necessary as we don't know the + /// target `Kiosk` upfront as the best bid or ask can change at any time. + /// + /// To resolve this, `Orderbook` creates a `TradeIntermediate` dynamic + /// field which can be permissionlessly resolved via this endpoint. + /// + /// See the documentation for `nft_protocol::transfer_request` to understand + /// how to deal with the returned [`TransferRequest`] type. + /// + /// #### Panics + /// + /// - Trade `ID` does not exist + /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless + /// buyer is the transaction sender. + public fun finish_sui_trade_inherit( + book: &mut Orderbook, + trade_id: ID, + seller_kiosk: &mut Kiosk, + buyer_kiosk: &mut Kiosk, + transfer_policy: &sui::transfer_policy::TransferPolicy, + ctx: &mut TxContext, + ): TransferRequest { + assert_version_and_upgrade(book); + + let trade = finalize_seller_side(book, trade_id, seller_kiosk); + let price = balance::value(&trade.paid); + let nft_id = trade.nft_id; + + let transfer_req = if (kiosk::is_locked(seller_kiosk, nft_id)) { + let paid = coin::take(&mut trade.paid, price, ctx); + ob_kiosk::transfer_delegated_locked( + seller_kiosk, + buyer_kiosk, + nft_id, + &book.id, + 0, + option::some(paid), + transfer_policy, + ctx, + ) + } else { + ob_kiosk::transfer_delegated_unlocked( + seller_kiosk, + buyer_kiosk, + nft_id, + &book.id, + price, + option::none(), + ctx, + ) + }; + + finalize_buyer_side(&mut transfer_req, trade, buyer_kiosk, ctx); + + transfer_req + } + + /// Optimistic equivalent of `finish_sui_trade_inherit` + /// + /// Executes a trade after orders have been matched but will not panic if + /// `Kiosks` do not match the trade ID. + /// + /// #### Panics + /// + /// - Trade `ID` does not exist + /// - Buyer's `Kiosk` does not allow permissionless deposits of `T` unless + /// buyer is the transaction sender. + public fun finish_sui_trade_inherit_if_kiosks_match( + book: &mut Orderbook, + trade_id: ID, + seller_kiosk: &mut Kiosk, + buyer_kiosk: &mut Kiosk, + transfer_policy: &sui::transfer_policy::TransferPolicy, + ctx: &mut TxContext + ): Option> { + // Version asserted by `finish_trade_inherit` + + let trade = trade(book, trade_id); + let kiosks_match = &trade.seller_kiosk == &object::id(seller_kiosk) + && &trade.buyer_kiosk == &object::id(buyer_kiosk); + + if (kiosks_match) { + option::some( + finish_sui_trade_inherit( + book, + trade_id, + seller_kiosk, + buyer_kiosk, + transfer_policy, + ctx + ) + ) + } else { + option::none() + } + } + /// Finalizes trade on the seller side by resolving `TradeIntermediate` /// /// #### Panics @@ -2084,12 +2415,13 @@ module liquidity_layer_v1::orderbook { ); option::destroy_none(maybe_commission); - let transfer_req = ob_kiosk::transfer_delegated( + let transfer_req = ob_kiosk::transfer_delegated_unlocked( seller_kiosk, buyer_kiosk, nft_id, &book.id, price, + option::none(), ctx, ); diff --git a/contracts/liquidity_layer_v1/tests/locked.move b/contracts/liquidity_layer_v1/tests/locked.move index 7ae92481..27ba2567 100644 --- a/contracts/liquidity_layer_v1/tests/locked.move +++ b/contracts/liquidity_layer_v1/tests/locked.move @@ -271,7 +271,7 @@ module liquidity_layer_v1::test_orderbook_locked { // 3. Perform trade on NFT and finish let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); - let request = orderbook::finish_trade_locked( + let request = orderbook::finish_sui_trade_locked( &mut orderbook, orderbook::trade_id(&trade), &mut seller_kiosk, @@ -310,6 +310,86 @@ module liquidity_layer_v1::test_orderbook_locked { // 3. Perform trade on NFT and finish let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); + let request = orderbook::finish_sui_trade_locked( + &mut orderbook, + orderbook::trade_id(&trade), + &mut seller_kiosk, + &mut buyer_kiosk, + &tx_policy, + ctx(&mut scenario), + ); + ob_request::transfer_request::confirm(request, &tx_policy, ctx(&mut scenario)); + test_scenario::return_shared(tx_policy); + + // 4. Verify locked + assert!(kiosk::is_locked(&buyer_kiosk, nft_id), 0); + + transfer::public_transfer(seller_kiosk, CREATOR); + transfer::public_transfer(buyer_kiosk, CREATOR); + test_scenario::return_shared(orderbook); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = liquidity_layer_v1::orderbook::EIncorrectEndpoint)] + fun test_transfer_unlocked_to_locked_generic() { + let scenario = test_scenario::begin(CREATOR); + + // 1. Create prerequisites + init_ob(&mut scenario); + let tx_policy = test_scenario::take_shared>(&mut scenario); + let orderbook = test_scenario::take_shared>(&scenario); + let (buyer_kiosk, _) = ob_kiosk::new_for_address(BUYER, ctx(&mut scenario)); + let (seller_kiosk, _) = ob_kiosk::new_for_address(SELLER, ctx(&mut scenario)); + + // 2. Add NFT to seller kiosk + let nft = Foo { id: object::new(ctx(&mut scenario)) }; + let nft_id = object::id(&nft); + ob_kiosk::deposit(&mut seller_kiosk, nft, ctx(&mut scenario)); + + // 3. Perform trade on NFT and finish + let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); + + let request = orderbook::finish_trade_locked( + &mut orderbook, + orderbook::trade_id(&trade), + &mut seller_kiosk, + &mut buyer_kiosk, + &tx_policy, + ctx(&mut scenario), + ); + ob_request::transfer_request::confirm(request, &tx_policy, ctx(&mut scenario)); + test_scenario::return_shared(tx_policy); + + // 4. Verify locked + assert!(kiosk::is_locked(&buyer_kiosk, nft_id), 0); + + transfer::public_transfer(seller_kiosk, CREATOR); + transfer::public_transfer(buyer_kiosk, CREATOR); + test_scenario::return_shared(orderbook); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = liquidity_layer_v1::orderbook::EIncorrectEndpoint)] + fun test_transfer_unlocked_to_locked_non_ob_policy_generic() { + let scenario = test_scenario::begin(CREATOR); + + // 1. Create prerequisites + init_non_ob(&mut scenario); + let tx_policy = test_scenario::take_shared>(&mut scenario); + let orderbook = test_scenario::take_shared>(&scenario); + let (buyer_kiosk, _) = ob_kiosk::new_for_address(BUYER, ctx(&mut scenario)); + let (seller_kiosk, _) = ob_kiosk::new_for_address(SELLER, ctx(&mut scenario)); + + // 2. Add NFT to seller kiosk + let nft = Foo { id: object::new(ctx(&mut scenario)) }; + let nft_id = object::id(&nft); + ob_kiosk::deposit(&mut seller_kiosk, nft, ctx(&mut scenario)); + + // 3. Perform trade on NFT and finish + let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); + let request = orderbook::finish_trade_locked( &mut orderbook, orderbook::trade_id(&trade), @@ -427,7 +507,7 @@ module liquidity_layer_v1::test_orderbook_locked { // 3. Perform trade on NFT and finish let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); - let request = orderbook::finish_trade_inherit( + let request = orderbook::finish_sui_trade_inherit( &mut orderbook, orderbook::trade_id(&trade), &mut seller_kiosk, @@ -466,6 +546,86 @@ module liquidity_layer_v1::test_orderbook_locked { // 3. Perform trade on NFT and finish let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); + let request = orderbook::finish_sui_trade_inherit( + &mut orderbook, + orderbook::trade_id(&trade), + &mut seller_kiosk, + &mut buyer_kiosk, + &tx_policy, + ctx(&mut scenario), + ); + ob_request::transfer_request::confirm(request, &tx_policy, ctx(&mut scenario)); + test_scenario::return_shared(tx_policy); + + // 4. Verify unlocked + assert!(!kiosk::is_locked(&buyer_kiosk, nft_id), 0); + + transfer::public_transfer(seller_kiosk, CREATOR); + transfer::public_transfer(buyer_kiosk, CREATOR); + test_scenario::return_shared(orderbook); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = liquidity_layer_v1::orderbook::EIncorrectEndpoint)] + fun test_transfer_unlocked_to_inherit_generic() { + let scenario = test_scenario::begin(CREATOR); + + // 1. Create prerequisites + init_ob(&mut scenario); + let tx_policy = test_scenario::take_shared>(&mut scenario); + let orderbook = test_scenario::take_shared>(&scenario); + let (buyer_kiosk, _) = ob_kiosk::new_for_address(BUYER, ctx(&mut scenario)); + let (seller_kiosk, _) = ob_kiosk::new_for_address(SELLER, ctx(&mut scenario)); + + // 2. Add NFT to seller kiosk + let nft = Foo { id: object::new(ctx(&mut scenario)) }; + let nft_id = object::id(&nft); + ob_kiosk::deposit(&mut seller_kiosk, nft, ctx(&mut scenario)); + + // 3. Perform trade on NFT and finish + let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); + + let request = orderbook::finish_trade_inherit( + &mut orderbook, + orderbook::trade_id(&trade), + &mut seller_kiosk, + &mut buyer_kiosk, + &tx_policy, + ctx(&mut scenario), + ); + ob_request::transfer_request::confirm(request, &tx_policy, ctx(&mut scenario)); + test_scenario::return_shared(tx_policy); + + // 4. Verify unlocked + assert!(!kiosk::is_locked(&buyer_kiosk, nft_id), 0); + + transfer::public_transfer(seller_kiosk, CREATOR); + transfer::public_transfer(buyer_kiosk, CREATOR); + test_scenario::return_shared(orderbook); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = liquidity_layer_v1::orderbook::EIncorrectEndpoint)] + fun test_transfer_unlocked_to_inherit_non_ob_policy_generic() { + let scenario = test_scenario::begin(CREATOR); + + // 1. Create prerequisites + init_non_ob(&mut scenario); + let tx_policy = test_scenario::take_shared>(&mut scenario); + let orderbook = test_scenario::take_shared>(&scenario); + let (buyer_kiosk, _) = ob_kiosk::new_for_address(BUYER, ctx(&mut scenario)); + let (seller_kiosk, _) = ob_kiosk::new_for_address(SELLER, ctx(&mut scenario)); + + // 2. Add NFT to seller kiosk + let nft = Foo { id: object::new(ctx(&mut scenario)) }; + let nft_id = object::id(&nft); + ob_kiosk::deposit(&mut seller_kiosk, nft, ctx(&mut scenario)); + + // 3. Perform trade on NFT and finish + let trade = init_trade(&mut orderbook, &mut seller_kiosk, &mut buyer_kiosk, nft_id, &mut scenario); + let request = orderbook::finish_trade_inherit( &mut orderbook, orderbook::trade_id(&trade), From e8de265ae91054c51bd41d081d1343390acaf8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=81abor?= Date: Wed, 19 Jul 2023 11:24:03 +0200 Subject: [PATCH 8/9] Review comment --- contracts/kiosk/sources/kiosk/ob_kiosk.move | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/kiosk/sources/kiosk/ob_kiosk.move b/contracts/kiosk/sources/kiosk/ob_kiosk.move index 0dd31669..bc389513 100644 --- a/contracts/kiosk/sources/kiosk/ob_kiosk.move +++ b/contracts/kiosk/sources/kiosk/ob_kiosk.move @@ -624,7 +624,7 @@ module ob_kiosk::ob_kiosk { req } - /// Deprecated, use `transfer_locked` instead + /// Deprecated, use `transfer_delegated` instead public fun transfer_locked_nft( _source: &mut Kiosk, _target: &mut Kiosk, From d7778e130cab0fab30457051cd2f93cf649b7ba7 Mon Sep 17 00:00:00 2001 From: Nuno Boavida <45330362+nmboavida@users.noreply.github.com> Date: Sat, 19 Aug 2023 12:20:15 +0100 Subject: [PATCH 9/9] Fix ob_kiosk tests --- contracts/tests/tests/kiosks/ob_kiosk.move | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/contracts/tests/tests/kiosks/ob_kiosk.move b/contracts/tests/tests/kiosks/ob_kiosk.move index 33462eef..210e35c6 100644 --- a/contracts/tests/tests/kiosks/ob_kiosk.move +++ b/contracts/tests/tests/kiosks/ob_kiosk.move @@ -23,6 +23,7 @@ // borrow #[test_only] module ob_tests::test_ob_kiosk { + use std::option; use sui::test_scenario::{Self, ctx}; use sui::kiosk::{Self, Kiosk}; use sui::transfer; @@ -268,12 +269,13 @@ module ob_tests::test_ob_kiosk { // Init Buyer's Kiosk let (buyer_kiosk, _) = ob_kiosk::new(ctx(&mut scenario)); // Transfer NFT and get - let request = ob_kiosk::transfer_delegated( + let request = ob_kiosk::transfer_delegated_unlocked( &mut kiosk, &mut buyer_kiosk, nft_id, &rand_entity, 0, + option::none(), ctx(&mut scenario) ); @@ -330,12 +332,13 @@ module ob_tests::test_ob_kiosk { // Init Buyer's Kiosk let (buyer_kiosk, _) = ob_kiosk::new(ctx(&mut scenario)); // Transfer NFT and get - let request = ob_kiosk::transfer_delegated( + let request = ob_kiosk::transfer_delegated_unlocked( &mut kiosk, &mut buyer_kiosk, nft_id, &rand_entity, 0, + option::none(), ctx(&mut scenario) ); @@ -391,12 +394,13 @@ module ob_tests::test_ob_kiosk { test_scenario::next_tx(&mut scenario, fake_address()); - let request = ob_kiosk::transfer_delegated( + let request = ob_kiosk::transfer_delegated_unlocked( &mut kiosk, &mut buyer_kiosk, nft_id, &rand_entity, 0, + option::none(), ctx(&mut scenario) ); @@ -537,11 +541,12 @@ module ob_tests::test_ob_kiosk { let (buyer_kiosk, _) = ob_kiosk::new(ctx(&mut scenario)); // Transfer NFT and get - let request = ob_kiosk::transfer_signed( + let request = ob_kiosk::transfer_signed_unlocked( &mut kiosk, &mut buyer_kiosk, nft_id, 0, + option::none(), ctx(&mut scenario) ); @@ -602,11 +607,12 @@ module ob_tests::test_ob_kiosk { assert!(tx_context::sender(ctx(&mut scenario)) == authorised_address, 0); // Transfer NFT and get - let request = ob_kiosk::transfer_signed( + let request = ob_kiosk::transfer_signed_unlocked( &mut kiosk, &mut buyer_kiosk, nft_id, 0, + option::none(), ctx(&mut scenario) ); @@ -664,11 +670,12 @@ module ob_tests::test_ob_kiosk { assert!(tx_context::sender(ctx(&mut scenario)) == unauthorised_address, 0); // Transfer NFT and get - let request = ob_kiosk::transfer_signed( + let request = ob_kiosk::transfer_signed_unlocked( &mut kiosk, &mut buyer_kiosk, nft_id, 0, + option::none(), ctx(&mut scenario) );