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] 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),