From 2f402c8b6293c5724edca0816fe6ce17d2877e50 Mon Sep 17 00:00:00 2001 From: porkbrain Date: Tue, 4 Oct 2022 19:46:20 +0200 Subject: [PATCH 01/16] Draft impl of safe in orderbook --- Move.toml | 1 - examples/custom_collection.move | 59 -------- examples/name_service.move | 82 ------------ sources/collection.move | 63 +++++++++ sources/orderbook.move | 229 +++++++++++++------------------- sources/safe.move | 62 +++++++++ 6 files changed, 216 insertions(+), 280 deletions(-) delete mode 100644 examples/custom_collection.move delete mode 100644 examples/name_service.move create mode 100644 sources/collection.move create mode 100644 sources/safe.move diff --git a/Move.toml b/Move.toml index f4655a8..eed5d3c 100644 --- a/Move.toml +++ b/Move.toml @@ -3,7 +3,6 @@ name = "Syrup" version = "0.4.0" [dependencies] -NftProtocol = { git = "https://github.com/Origin-Byte/nft-protocol", rev = "0.3.0" } Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework", rev = "devnet-0.9.0" } [addresses] diff --git a/examples/custom_collection.move b/examples/custom_collection.move deleted file mode 100644 index 8a06580..0000000 --- a/examples/custom_collection.move +++ /dev/null @@ -1,59 +0,0 @@ -module syrup::custom_collection { - //! A simple showcase how to participate in the liquidity layer as a custom - //! collection implementor. - //! - //! The common use case to trade art NFTs would be implemented by the - //! `StdCollection` of the OriginByte nft protocol (will be a standalone - //! contract so no circular dependencies.) - //! - //! In a nutshell, the `StdCollection` implements a default desired behavior - //! for creators to list their collections in the liquidity layer. - //! - //! It would implements royalty distribution based on a business logic which - //! makes most sense for art NFTs. - - use syrup::orderbook::{Self, Orderbook}; - use std::fixed_point32; - use sui::object::{Self, UID}; - use sui::balance; - use sui::coin; - use sui::transfer::{Self, transfer}; - use sui::sui::SUI; - use sui::tx_context::{Self, TxContext}; - - // TODO: remove `store` once new version of nft-protocol lands - struct Witness has drop, store {} - - struct MyCollection has key, store { - id: UID, - admin: address, - } - - public entry fun create_orderbook( - my_collection: &MyCollection, - ctx: &mut TxContext, - ) { - assert!(tx_context::sender(ctx) == my_collection.admin, 0); - - let royalty = fixed_point32::create_from_rational(1, 100); // 1% - - transfer::share_object(orderbook::create( - Witness {}, - object::id(my_collection), - royalty, - ctx, - )); - } - - public entry fun collect_royalties( - my_collection: &MyCollection, - ob: &mut Orderbook, - ctx: &mut TxContext, - ) { - assert!(tx_context::sender(ctx) == my_collection.admin, 0); - - let balance = orderbook::fees_balance(Witness {}, ob); - let total = balance::value(balance); - transfer(coin::take(balance, total, ctx), my_collection.admin); - } -} diff --git a/examples/name_service.move b/examples/name_service.move deleted file mode 100644 index dbaabc9..0000000 --- a/examples/name_service.move +++ /dev/null @@ -1,82 +0,0 @@ -module syrup::name_service { - //! This example showcases custom implementation for logic creating an ask. - //! - //! Now, marketplaces and wallets have two options: implement creating ask - //! for Sui NS (via the standard e.g.) or decide to only enable bids and - //! buying of name addreses on their platform, but skip selling. - //! - //! We name this "name_service" because one of the teams building a NS on - //! Sui wanted to make trading of expired NFTs impossible. - - use nft_protocol::collection::{Self, Collection}; - use nft_protocol::nft::NftOwned; - use syrup::orderbook::{Self, Orderbook}; - use std::fixed_point32; - use std::option::Option; - use std::string::String; - use sui::object::{Self, ID, UID}; - use sui::transfer; - use sui::tx_context::{Self, TxContext}; - - // witness pattern - // TODO: remove `store` once new version of nft-protocol lands - struct NS has drop, store {} - - // user NFT data - struct NSNftMetadata has key, store { - id: UID, - // points to `NSDomain` - domain: ID, - // some expiry info which will be used in `fun is_expired` - expires_at: u64, - } - - // holds global information about the NS collection, such as tld, registered - // domains etc - struct NSCollMetadata has store { - admin: address, - registered_domains: vector, - } - - // info specific to a domain that's global, owned e.g. by the collection - // object or shared and only accessible by some authority - struct NSDomain has key { - id: UID, - name: String, - current_nft: Option, - } - - fun is_expired(_nft: &NftOwned): bool { - // TODO: implement logic for determining if the domain is expired - - false - } - - /// Joins the NFT liquidity layer but restricts asks such that they can only - /// be called via this contract. - public entry fun create_orderbook( - col: &Collection, - ctx: &mut TxContext, - ) { - let sender = tx_context::sender(ctx); - assert!(collection::metadata(col).admin == sender, 0); - - let fee = fixed_point32::create_from_rational(1, 100); // 1% - - let ob = orderbook::create(NS {}, object::id(col), fee, ctx); - orderbook::toggle_protection_on_create_ask(NS {}, &mut ob); - - transfer::share_object(ob); - } - - /// The NFT can only be listed for sale if the domain is not expired. - public entry fun create_ask( - book: &mut Orderbook, - requsted_tokens: u64, - nft: NftOwned, - ctx: &mut TxContext, - ) { - assert!(!is_expired(&nft), 0); - orderbook::create_ask_protected(NS {}, book, requsted_tokens, nft, ctx) - } -} diff --git a/sources/collection.move b/sources/collection.move new file mode 100644 index 0000000..ab1ad2f --- /dev/null +++ b/sources/collection.move @@ -0,0 +1,63 @@ +//! Taken from https://github.com/MystenLabs/sui/pull/4887/files#diff-4097e0ffb7703cda3da51b586eba7657dfc6bfe919ca2b1be060aca6d71e8cd2 +//! and modified. + +module syrup::collection { + use sui::balance::Balance; + use sui::object::{Self, ID, UID}; + use sui::transfer::transfer_to_object; + use sui::tx_context::TxContext; + + struct Collection has key, store { + id: UID, + } + + /// Proof that the given NFT is one of the limited `total_supply` NFT's in `Collection` + struct CollectionProof has store { + collection_id: ID + } + + struct Trade has key { + id: UID, + } + + struct TradePayment has key { + id: UID, + /// Same as `Trade::id` if the NFT was sold for just one FT kind. + /// However, can bind multiple `Trade` object with a different generic + /// into one logical unit if the royalty logic requires e.g. payment in + /// both `SUI` and `USDC`. + trade: ID, + amount: Balance + } + + /// Resolve the trade with [`safe::trade_nft`] + public fun begin_nft_trade_with( + amount: Balance, + ctx: &mut TxContext, + ): Trade { + let trade = begin_nft_trade(ctx); + pay_for_nft(&mut trade, amount, ctx); + + trade + } + + /// Resolve the trade with [`safe::trade_nft`] + public fun begin_nft_trade(ctx: &mut TxContext): Trade { + Trade { + id: object::new(ctx), + } + } + + public fun pay_for_nft( + trade: &mut Trade, + amount: Balance, + ctx: &mut TxContext, + ) { + let payment = TradePayment { + id: object::new(ctx), + trade: object::id(trade), + amount, + }; + transfer_to_object(payment, trade); + } +} diff --git a/sources/orderbook.move b/sources/orderbook.move index 4920bd6..26f1012 100644 --- a/sources/orderbook.move +++ b/sources/orderbook.move @@ -16,29 +16,28 @@ module syrup::orderbook { // TODO: collect fees on trade - use nft_protocol::nft::{Self, NftOwned}; use syrup::err; use std::fixed_point32::FixedPoint32; use std::vector; use sui::balance::{Self, Balance}; use sui::coin::{Self, Coin}; use sui::object::{Self, ID, UID}; - use sui::transfer::{transfer, transfer_to_object}; + use sui::transfer::transfer; use sui::tx_context::{Self, TxContext}; use syrup::crit_bit::{Self, CB as CBTree}; + use syrup::collection; + use syrup::safe::{Self, Safe, TransferCap}; /// A critbit order book implementation. Contains two ordered trees: /// 1. bids ASC /// 2. asks DESC - struct Orderbook has key { + struct Orderbook has key { id: UID, - /// Only NFTs belonging to this collection can be traded. - collection: ID, /// Actions which have a flag set to true can only be called via a /// witness protected implementation. protected_actions: WitnessProtectedActions, /// Collects fees in fungible tokens. - fees: Fees, + fees: Fees, /// An ask order stores an NFT to be traded. The price associated with /// such an order is saying: /// @@ -51,7 +50,7 @@ module syrup::orderbook { bids: CBTree>>, } - struct Fees has store { + struct Fees has store { /// All fees during trading are collected here and the witness protected /// method [`collect_fees`] is implemented by downstream packages. /// @@ -107,8 +106,8 @@ module syrup::orderbook { id: UID, /// How many tokens does the seller want for their NFT in exchange. price: u64, - /// The pointer to the offered NFT. - nft: ID, + /// Capability to get an NFT from a safe. + nft_cap: TransferCap, /// Who owns the NFT. owner: address, } @@ -120,8 +119,8 @@ module syrup::orderbook { /// If the `price` is higher than the lowest ask requested price, then we /// execute a trade straight away. Otherwise we add the bid to the /// orderbook's state. - public entry fun create_bid( - book: &mut Orderbook, + public entry fun create_bid( + book: &mut Orderbook, price: u64, wallet: &mut Coin, ctx: &mut TxContext, @@ -129,9 +128,9 @@ module syrup::orderbook { assert!(book.protected_actions.create_bid, err::action_not_public()); create_bid_(book, price, wallet, ctx) } - public fun create_bid_protected( - _witness: C, - book: &mut Orderbook, + public fun create_bid_protected( + _witness: Wness, + book: &mut Orderbook, price: u64, wallet: &mut Coin, ctx: &mut TxContext, @@ -141,8 +140,8 @@ module syrup::orderbook { /// Cancel a bid owned by the sender at given price. If there are two bids /// with the same price, the one created later is cancelled. - public entry fun cancel_bid( - book: &mut Orderbook, + public entry fun cancel_bid( + book: &mut Orderbook, requested_bid_offer_to_cancel: u64, wallet: &mut Coin, ctx: &mut TxContext, @@ -150,9 +149,9 @@ module syrup::orderbook { assert!(book.protected_actions.cancel_bid, err::action_not_public()); cancel_bid_(book, requested_bid_offer_to_cancel, wallet, ctx) } - public fun cancel_bid_protected( - _witness: C, - book: &mut Orderbook, + public fun cancel_bid_protected( + _witness: Wness, + book: &mut Orderbook, requested_bid_offer_to_cancel: u64, wallet: &mut Coin, ctx: &mut TxContext, @@ -164,23 +163,25 @@ module syrup::orderbook { /// there exists a bid with higher offer than `requsted_tokens`, then trade /// is immeidately executed. Otherwise the NFT is transferred to a newly /// created ask object and the object is inserted to the orderbook. - public entry fun create_ask( - book: &mut Orderbook, + public entry fun create_ask( + book: &mut Orderbook, requsted_tokens: u64, - nft: NftOwned, + nft_cap: TransferCap, + safe: &mut Safe, ctx: &mut TxContext, ) { assert!(book.protected_actions.create_ask, err::action_not_public()); - create_ask_(book, requsted_tokens, nft, ctx) + create_ask_(book, requsted_tokens, nft_cap, safe, ctx) } - public fun create_ask_protected( - _witness: C, - book: &mut Orderbook, + public fun create_ask_protected( + _witness: Wness, + book: &mut Orderbook, requsted_tokens: u64, - nft: NftOwned, + nft_cap: TransferCap, + safe: &mut Safe, ctx: &mut TxContext, ) { - create_ask_(book, requsted_tokens, nft, ctx) + create_ask_(book, requsted_tokens, nft_cap, safe, ctx) } /// We could remove the NFT requested price from the argument, but then the @@ -188,8 +189,8 @@ module syrup::orderbook { /// /// This API might be improved in future as we use a different data /// structure for the orderbook. - public entry fun cancel_ask( - book: &mut Orderbook, + public entry fun cancel_ask( + book: &mut Orderbook, requested_price_to_cancel: u64, nft_id: ID, ctx: &mut TxContext, @@ -197,9 +198,9 @@ module syrup::orderbook { assert!(book.protected_actions.cancel_ask, err::action_not_public()); cancel_ask_(book, requested_price_to_cancel, nft_id, ctx) } - public entry fun cancel_ask_protected( - _witness: C, - book: &mut Orderbook, + public entry fun cancel_ask_protected( + _witness: Wness, + book: &mut Orderbook, requested_price_to_cancel: u64, nft_id: ID, ctx: &mut TxContext, @@ -210,8 +211,8 @@ module syrup::orderbook { /// Buys a specific NFT from the orderbook. This is an atypical OB API as /// with fungible tokens, you just want to get the cheapest ask. /// However, with NFTs, you might want to get a specific one. - public entry fun buy_nft( - book: &mut Orderbook, + public entry fun buy_nft( + book: &mut Orderbook, nft_id: ID, price: u64, wallet: &mut Coin, @@ -220,9 +221,9 @@ module syrup::orderbook { assert!(book.protected_actions.buy_nft, err::action_not_public()); buy_nft_(book, nft_id, price, wallet, ctx) } - public entry fun buy_nft_protected( - _witness: C, - book: &mut Orderbook, + public entry fun buy_nft_protected( + _witness: Wness, + book: &mut Orderbook, nft_id: ID, price: u64, wallet: &mut Coin, @@ -231,17 +232,6 @@ module syrup::orderbook { buy_nft_(book, nft_id, price, wallet, ctx) } - /// After a bid is matched with an ask, the buyer can claim the NFT via this - /// method, because they will have received the ownership of the - /// corresponding [`Ask`] object. - public entry fun claim_nft( - ask: Ask, - nft: NftOwned, - ctx: &mut TxContext, - ) { - claim_nft_(ask, nft, ctx) - } - /// `C`ollection kind of NFTs to be traded, and `F`ungible `T`oken to be /// quoted for an NFT in such a collection. /// @@ -251,46 +241,45 @@ module syrup::orderbook { /// To implement specific logic in your smart contract, you can toggle the /// protection on specific actions. That will make them only accessible via /// witness protected methods. - public fun create( - _witness: C, - collection: ID, + public fun create( + _witness: Wness, fee: FixedPoint32, ctx: &mut TxContext, - ): Orderbook { - create_(collection, fee, no_protection(), ctx) + ): Orderbook { + create_(fee, no_protection(), ctx) } - public fun toggle_protection_on_buy_nft( - _witness: C, - book: &mut Orderbook, + public fun toggle_protection_on_buy_nft( + _witness: Wness, + book: &mut Orderbook, ) { book.protected_actions.buy_nft = !book.protected_actions.buy_nft; } - public fun toggle_protection_on_cancel_ask( - _witness: C, - book: &mut Orderbook, + public fun toggle_protection_on_cancel_ask( + _witness: Wness, + book: &mut Orderbook, ) { book.protected_actions.cancel_ask = !book.protected_actions.cancel_ask; } - public fun toggle_protection_on_cancel_bid( - _witness: C, - book: &mut Orderbook, + public fun toggle_protection_on_cancel_bid( + _witness: Wness, + book: &mut Orderbook, ) { book.protected_actions.cancel_bid = !book.protected_actions.cancel_bid; } - public fun toggle_protection_on_create_ask( - _witness: C, - book: &mut Orderbook, + public fun toggle_protection_on_create_ask( + _witness: Wness, + book: &mut Orderbook, ) { book.protected_actions.create_ask = !book.protected_actions.create_ask; } - public fun toggle_protection_on_create_bid( - _witness: C, - book: &mut Orderbook, + public fun toggle_protection_on_create_bid( + _witness: Wness, + book: &mut Orderbook, ) { book.protected_actions.create_bid = !book.protected_actions.create_bid; @@ -298,21 +287,15 @@ module syrup::orderbook { /// The contract which instantiated the OB implements logic for distibuting /// the fee based on its requirements. - public fun fees_balance( - _witness: C, - orderbook: &mut Orderbook, + public fun fees_balance( + _witness: Wness, + orderbook: &mut Orderbook, ): &mut Balance { fees_balance_(orderbook) } - public fun collection_id( - book: &Orderbook, - ): ID { - book.collection - } - - public fun borrow_bids( - book: &Orderbook, + public fun borrow_bids( + book: &Orderbook, ): &CBTree>> { &book.bids } @@ -325,8 +308,8 @@ module syrup::orderbook { bid.owner } - public fun borrow_asks( - book: &Orderbook, + public fun borrow_asks( + book: &Orderbook, ): &CBTree> { &book.asks } @@ -335,29 +318,27 @@ module syrup::orderbook { ask.price } - public fun ask_nft_id(ask: &Ask): ID { - ask.nft + public fun ask_nft(ask: &Ask): &TransferCap { + &ask.nft_cap } public fun ask_owner(ask: &Ask): address { ask.owner } - fun create_( - collection: ID, + fun create_( fee: FixedPoint32, protected_actions: WitnessProtectedActions, ctx: &mut TxContext, - ): Orderbook { + ): Orderbook { let id = object::new(ctx); let fees = Fees { uncollected: balance::zero(), fee, }; - Orderbook { + Orderbook { id, - collection, fees, protected_actions, asks: crit_bit::empty(), @@ -365,14 +346,14 @@ module syrup::orderbook { } } - fun fees_balance_( - orderbook: &mut Orderbook, + fun fees_balance_( + orderbook: &mut Orderbook, ): &mut Balance { &mut orderbook.fees.uncollected } - fun create_bid_( - book: &mut Orderbook, + fun create_bid_( + book: &mut Orderbook, price: u64, wallet: &mut Coin, ctx: &mut TxContext, @@ -422,8 +403,8 @@ module syrup::orderbook { } } - fun cancel_bid_( - book: &mut Orderbook, + fun cancel_bid_( + book: &mut Orderbook, requested_bid_offer_to_cancel: u64, wallet: &mut Coin, ctx: &mut TxContext, @@ -461,14 +442,15 @@ module syrup::orderbook { ); } - fun create_ask_( - book: &mut Orderbook, + fun create_ask_( + book: &mut Orderbook, price: u64, - nft: NftOwned, + nft_cap: TransferCap, + safe: &mut Safe, ctx: &mut TxContext, ) { assert!( - nft::collection_id(&nft) == book.collection, + object::id(safe) == safe::transfer_cap_safe_id(&nft_cap), err::nft_collection_mismatch(), ); @@ -497,20 +479,17 @@ module syrup::orderbook { owner: buyer, offer: bid_offer, } = bid; - // transfer FT to NFT owner - transfer(coin::from_balance(bid_offer, ctx), seller); - // transfer NFT to FT owner - transfer(nft, buyer); // TODO: use the nft transfer fn + let trade = collection::begin_nft_trade_with(bid_offer, ctx); + let nft = safe::trade_nft(nft_cap, trade, safe); + transfer(nft, buyer); } else { let id = object::new(ctx); let ask = Ask { id, price, - nft: object::id(&nft), owner: seller, + nft_cap, }; - // the NFT is now owned by the Ask object - transfer_to_object(nft, &mut ask); // store the Ask object if (crit_bit::has_key(&book.asks, price)) { vector::push_back( @@ -527,8 +506,8 @@ module syrup::orderbook { } } - fun cancel_ask_( - book: &mut Orderbook, + fun cancel_ask_( + book: &mut Orderbook, requested_price_to_cancel: u64, nft_id: ID, ctx: &mut TxContext, @@ -548,8 +527,8 @@ module syrup::orderbook { transfer(ask, sender); } - fun buy_nft_( - book: &mut Orderbook, + fun buy_nft_( + book: &mut Orderbook, nft_id: ID, price: u64, wallet: &mut Coin, @@ -571,32 +550,6 @@ module syrup::orderbook { transfer(ask, buyer); } - /// The NFT is owned by the ask object, the ownership is transferred in - /// the [`create_ask`] function. - /// Here, we destruct the ask and give the ownership of the NFT to the owner - /// of the ask object. - /// To become an owner of an ask object, one has to create a bid which is - /// filled. - fun claim_nft_( - ask: Ask, - nft: NftOwned, - ctx: &mut TxContext, - ) { - let sender = tx_context::sender(ctx); - - let Ask { - nft: nft_id, - id: ask_id, - price: _, - owner: _, - } = ask; - - assert!(nft_id == object::id(&nft), err::nft_id_mismatch()); - - object::delete(ask_id); - transfer(nft, sender); // TODO: transfer must be done by the nft module - } - /// 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 { @@ -612,14 +565,14 @@ module syrup::orderbook { while (asks_count > index) { let ask = vector::borrow(price_level, index); // on the same price level, we search for the specified NFT - if (nft_id == ask.nft) { + if (nft_id == safe::transfer_cap_nft_id(&ask.nft_cap)) { break }; index = index + 1; }; - assert!(index < asks_count, err::order_owner_must_be_sender()); + assert!(index < asks_count, err::order_does_not_exist()); vector::remove(price_level, index) } diff --git a/sources/safe.move b/sources/safe.move new file mode 100644 index 0000000..ffb1214 --- /dev/null +++ b/sources/safe.move @@ -0,0 +1,62 @@ +//! Taken from https://github.com/MystenLabs/sui/pull/4887/files#diff-96b0dc07cabd79292618c993cd473c43ca81cd2f742266014967cdea1a7c6186 +//! and modified. + +module syrup::safe { + use sui::object::{ID, UID}; + use sui::transfer::transfer_to_object; + use syrup::collection::{CollectionProof, Trade}; + + /// A shared object for storing NFT's of type `T`, owned by the holder of a unique `OwnerCap`. + /// Permissions to allow others to list NFT's can be granted via TransferCap's and BorrowCap's + struct Safe has key { + id: UID, + } + + /// A unique capability held by the owner of a particular `Safe`. + /// The holder can issue and revoke `TransferCap`'s and `BorrowCap`'s. + /// Can be used an arbitrary number of times + struct OwnerCap has key, store { + id: UID, + /// The ID of the safe that this capability grants permissions to + safe_id: ID, + } + + /// Gives the holder permission to transfer the nft with id `nft_id` out of + /// the safe with id `safe_id`. Can only be used once. + struct TransferCap has key, store { + id: UID, + /// The ID of the safe that this capability grants permissions to + safe_id: ID, + /// The ID of the NFT that this capability can transfer + nft_id: ID, + proof: CollectionProof, + } + + /// Produce a `TransferCap` for the NFT with `id` in `safe`. + /// This `TransferCap` can be (e.g.) used to list the NFT on a marketplace. + public fun sell_nft(_owner_cap: &OwnerCap, _id: ID, _safe: &mut T): TransferCap { + abort(0) + } + + /// Consume `cap`, remove the NFT with `id` from `safe`, and return it to the caller. + /// Requiring `royalty` ensures that the caller has paid the required royalty for this collection + /// before completing the purchase. + /// This invalidates all other `TransferCap`'s by increasing safe.transfer_cap_version + public fun trade_nft( + _cap: TransferCap, + trade: Trade, + safe: &mut Safe, + ): T { + transfer_to_object(trade, safe); + + abort(0) + } + + public fun transfer_cap_safe_id(cap: &TransferCap): ID { + cap.safe_id + } + + public fun transfer_cap_nft_id(cap: &TransferCap): ID { + cap.nft_id + } +} From baa1bbb234fdaa4582dda776d30c49a32c8b2ef2 Mon Sep 17 00:00:00 2001 From: porkbrain Date: Tue, 4 Oct 2022 19:48:07 +0200 Subject: [PATCH 02/16] Removing Fees generic --- sources/orderbook.move | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sources/orderbook.move b/sources/orderbook.move index 26f1012..1700818 100644 --- a/sources/orderbook.move +++ b/sources/orderbook.move @@ -37,7 +37,7 @@ module syrup::orderbook { /// witness protected implementation. protected_actions: WitnessProtectedActions, /// Collects fees in fungible tokens. - fees: Fees, + fees: Fees, /// An ask order stores an NFT to be traded. The price associated with /// such an order is saying: /// @@ -50,7 +50,7 @@ module syrup::orderbook { bids: CBTree>>, } - struct Fees has store { + struct Fees has store { /// All fees during trading are collected here and the witness protected /// method [`collect_fees`] is implemented by downstream packages. /// From d7406dd681abba4a4f05596648ae1c2e584435ca Mon Sep 17 00:00:00 2001 From: porkbrain Date: Tue, 4 Oct 2022 20:00:38 +0200 Subject: [PATCH 03/16] Adding logic to buy_nft and cancel_ask --- sources/orderbook.move | 60 ++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/sources/orderbook.move b/sources/orderbook.move index 1700818..fee997d 100644 --- a/sources/orderbook.move +++ b/sources/orderbook.move @@ -53,9 +53,6 @@ module syrup::orderbook { struct Fees has store { /// All fees during trading are collected here and the witness protected /// method [`collect_fees`] is implemented by downstream packages. - /// - /// For example, in case of standard art collections, these fees - /// represent royalties. uncollected: Balance, /// in iterval `[0; 1)` fee: FixedPoint32, @@ -123,6 +120,7 @@ module syrup::orderbook { book: &mut Orderbook, price: u64, wallet: &mut Coin, + // TODO: ask for Safe Or exlusive transfer cap ctx: &mut TxContext, ) { assert!(book.protected_actions.create_bid, err::action_not_public()); @@ -191,45 +189,47 @@ module syrup::orderbook { /// structure for the orderbook. public entry fun cancel_ask( book: &mut Orderbook, - requested_price_to_cancel: u64, + nft_price: u64, nft_id: ID, ctx: &mut TxContext, ) { assert!(book.protected_actions.cancel_ask, err::action_not_public()); - cancel_ask_(book, requested_price_to_cancel, nft_id, ctx) + cancel_ask_(book, nft_price, nft_id, ctx) } public entry fun cancel_ask_protected( _witness: Wness, book: &mut Orderbook, - requested_price_to_cancel: u64, + nft_price: u64, nft_id: ID, ctx: &mut TxContext, ) { - cancel_ask_(book, requested_price_to_cancel, nft_id, ctx) + cancel_ask_(book, nft_price, nft_id, ctx) } /// Buys a specific NFT from the orderbook. This is an atypical OB API as /// with fungible tokens, you just want to get the cheapest ask. /// However, with NFTs, you might want to get a specific one. - public entry fun buy_nft( + public entry fun buy_nft( book: &mut Orderbook, nft_id: ID, price: u64, wallet: &mut Coin, + safe: &mut Safe, ctx: &mut TxContext, ) { assert!(book.protected_actions.buy_nft, err::action_not_public()); - buy_nft_(book, nft_id, price, wallet, ctx) + buy_nft_(book, nft_id, price, wallet, safe, ctx) } - public entry fun buy_nft_protected( + public entry fun buy_nft_protected( _witness: Wness, book: &mut Orderbook, nft_id: ID, price: u64, wallet: &mut Coin, + safe: &mut Safe, ctx: &mut TxContext, ) { - buy_nft_(book, nft_id, price, wallet, ctx) + buy_nft_(book, nft_id, price, wallet, safe, ctx) } /// `C`ollection kind of NFTs to be traded, and `F`ungible `T`oken to be @@ -508,46 +508,56 @@ module syrup::orderbook { fun cancel_ask_( book: &mut Orderbook, - requested_price_to_cancel: u64, + nft_price: u64, nft_id: ID, ctx: &mut TxContext, ) { let sender = tx_context::sender(ctx); - let ask = remove_ask( + let Ask { + owner, + id, + price: _, + nft_cap + } = remove_ask( &mut book.asks, - requested_price_to_cancel, + nft_price, nft_id, ); - assert!(ask.owner != sender, err::order_owner_must_be_sender()); + assert!(owner != sender, err::order_owner_must_be_sender()); - // TODO: figure out whether we can provide NftOwned here and do the - // transfer without the intermediary step - transfer(ask, sender); + object::delete(id); + transfer(nft_cap, sender); } - fun buy_nft_( + fun buy_nft_( book: &mut Orderbook, nft_id: ID, price: u64, wallet: &mut Coin, + safe: &mut Safe, ctx: &mut TxContext, ) { let buyer = tx_context::sender(ctx); - let ask = remove_ask( + let Ask { + id, + nft_cap, + owner: _, + price: _, + } = remove_ask( &mut book.asks, price, nft_id, ); + object::delete(id); - // pay the NFT owner - coin::split_and_transfer(wallet, price, ask.owner, ctx); + let offer = balance::split(coin::balance_mut(wallet), price); - // TODO: figure out whether we can provide NftOwned here and do the - // transfer without the intermediary step - transfer(ask, buyer); + let trade = collection::begin_nft_trade_with(offer, ctx); + let nft = safe::trade_nft(nft_cap, trade, safe); + transfer(nft, buyer); } /// Finds an ask of a given NFT advertized for the given price. Removes it From 43af1f021697fc9ef09f4f1ee9a83bee2faf9185 Mon Sep 17 00:00:00 2001 From: porkbrain Date: Thu, 6 Oct 2022 10:33:02 +0200 Subject: [PATCH 04/16] Adding witness pattern for Trade --- sources/collection.move | 14 ++++---- sources/crit_bit.move | 1 + sources/err.move | 4 +++ sources/orderbook.move | 80 ++++++++++++++++------------------------- sources/safe.move | 25 +++++++------ 5 files changed, 58 insertions(+), 66 deletions(-) diff --git a/sources/collection.move b/sources/collection.move index ab1ad2f..8432907 100644 --- a/sources/collection.move +++ b/sources/collection.move @@ -16,7 +16,7 @@ module syrup::collection { collection_id: ID } - struct Trade has key { + struct Trade has key { id: UID, } @@ -27,14 +27,14 @@ module syrup::collection { /// into one logical unit if the royalty logic requires e.g. payment in /// both `SUI` and `USDC`. trade: ID, - amount: Balance + amount: Balance, } /// Resolve the trade with [`safe::trade_nft`] - public fun begin_nft_trade_with( + public fun begin_nft_trade_with( amount: Balance, ctx: &mut TxContext, - ): Trade { + ): Trade { let trade = begin_nft_trade(ctx); pay_for_nft(&mut trade, amount, ctx); @@ -42,14 +42,14 @@ module syrup::collection { } /// Resolve the trade with [`safe::trade_nft`] - public fun begin_nft_trade(ctx: &mut TxContext): Trade { + public fun begin_nft_trade(ctx: &mut TxContext): Trade { Trade { id: object::new(ctx), } } - public fun pay_for_nft( - trade: &mut Trade, + public fun pay_for_nft( + trade: &mut Trade, amount: Balance, ctx: &mut TxContext, ) { diff --git a/sources/crit_bit.move b/sources/crit_bit.move index 4df815b..071f17e 100644 --- a/sources/crit_bit.move +++ b/sources/crit_bit.move @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copied from: https://github.com/econia-labs/econia/blob/main/src/move/econia/sources/CritBit.move +// TODO: try again to use movemate /// # Module-level documentation sections /// diff --git a/sources/err.move b/sources/err.move index bbe360a..7dc9ab1 100644 --- a/sources/err.move +++ b/sources/err.move @@ -23,4 +23,8 @@ module syrup::err { public fun action_not_public(): u64 { return Prefix + 04 } + + public fun nft_not_exclusive(): u64 { + return Prefix + 05 + } } diff --git a/sources/orderbook.move b/sources/orderbook.move index fee997d..c031f27 100644 --- a/sources/orderbook.move +++ b/sources/orderbook.move @@ -6,7 +6,6 @@ module syrup::orderbook { //! One can //! - create a new orderbook between a given collection and a bid token //! (witness pattern protected); - //! - collect fees (witness pattern protected); //! - set publicly accessible actions to be witness protected; //! - open a new bid; //! - cancel an existing bid they own; @@ -14,18 +13,18 @@ module syrup::orderbook { //! - cancel an existing NFT offer; //! - instantly buy an specific NFT. - // TODO: collect fees on trade + // TODO: collect commision on trade + // TODO: protocol toll - use syrup::err; - use std::fixed_point32::FixedPoint32; use std::vector; use sui::balance::{Self, Balance}; use sui::coin::{Self, Coin}; use sui::object::{Self, ID, UID}; use sui::transfer::transfer; use sui::tx_context::{Self, TxContext}; - use syrup::crit_bit::{Self, CB as CBTree}; use syrup::collection; + use syrup::crit_bit::{Self, CB as CBTree}; + use syrup::err; use syrup::safe::{Self, Safe, TransferCap}; /// A critbit order book implementation. Contains two ordered trees: @@ -36,8 +35,6 @@ module syrup::orderbook { /// Actions which have a flag set to true can only be called via a /// witness protected implementation. protected_actions: WitnessProtectedActions, - /// Collects fees in fungible tokens. - fees: Fees, /// An ask order stores an NFT to be traded. The price associated with /// such an order is saying: /// @@ -50,14 +47,6 @@ module syrup::orderbook { bids: CBTree>>, } - struct Fees has store { - /// All fees during trading are collected here and the witness protected - /// method [`collect_fees`] is implemented by downstream packages. - uncollected: Balance, - /// in iterval `[0; 1)` - fee: FixedPoint32, - } - /// The contract which creates the orderbook can restrict specific actions /// to be only callable with a witness pattern and not via the entry point /// function. @@ -116,24 +105,25 @@ module syrup::orderbook { /// If the `price` is higher than the lowest ask requested price, then we /// execute a trade straight away. Otherwise we add the bid to the /// orderbook's state. - public entry fun create_bid( + public entry fun create_bid( book: &mut Orderbook, price: u64, wallet: &mut Coin, - // TODO: ask for Safe Or exlusive transfer cap + safe: &mut Safe, // !!! ctx: &mut TxContext, ) { assert!(book.protected_actions.create_bid, err::action_not_public()); - create_bid_(book, price, wallet, ctx) + create_bid_(book, price, wallet, safe, ctx) } - public fun create_bid_protected( + public fun create_bid_protected( _witness: Wness, book: &mut Orderbook, price: u64, wallet: &mut Coin, + safe: &mut Safe, // !!! ctx: &mut TxContext, ) { - create_bid_(book, price, wallet, ctx) + create_bid_(book, price, wallet, safe, ctx) } /// Cancel a bid owned by the sender at given price. If there are two bids @@ -243,10 +233,9 @@ module syrup::orderbook { /// witness protected methods. public fun create( _witness: Wness, - fee: FixedPoint32, ctx: &mut TxContext, ): Orderbook { - create_(fee, no_protection(), ctx) + create_(no_protection(), ctx) } public fun toggle_protection_on_buy_nft( @@ -285,15 +274,6 @@ module syrup::orderbook { !book.protected_actions.create_bid; } - /// The contract which instantiated the OB implements logic for distibuting - /// the fee based on its requirements. - public fun fees_balance( - _witness: Wness, - orderbook: &mut Orderbook, - ): &mut Balance { - fees_balance_(orderbook) - } - public fun borrow_bids( book: &Orderbook, ): &CBTree>> { @@ -327,35 +307,24 @@ module syrup::orderbook { } fun create_( - fee: FixedPoint32, protected_actions: WitnessProtectedActions, ctx: &mut TxContext, ): Orderbook { let id = object::new(ctx); - let fees = Fees { - uncollected: balance::zero(), - fee, - }; Orderbook { id, - fees, protected_actions, asks: crit_bit::empty(), bids: crit_bit::empty(), } } - fun fees_balance_( - orderbook: &mut Orderbook, - ): &mut Balance { - &mut orderbook.fees.uncollected - } - - fun create_bid_( + fun create_bid_( book: &mut Orderbook, price: u64, wallet: &mut Coin, + safe: &mut Safe, // !!! ctx: &mut TxContext, ) { let buyer = tx_context::sender(ctx); @@ -383,8 +352,17 @@ module syrup::orderbook { vector::destroy_empty(crit_bit::pop(asks, lowest_ask_price)); }; - transfer(coin::from_balance(bid_offer, ctx), ask.owner); - transfer(ask, buyer); + let Ask { + id, + price: _, + owner: seller, + nft_cap, + } = ask; + assert!(safe::safe_owner(safe) == seller, 0); + let trade = collection::begin_nft_trade_with(bid_offer, ctx); + let nft = safe::trade_nft(nft_cap, trade, safe); + transfer(nft, buyer); + object::delete(id); } else { let order = Bid { offer: bid_offer, owner: buyer }; @@ -453,13 +431,17 @@ module syrup::orderbook { object::id(safe) == safe::transfer_cap_safe_id(&nft_cap), err::nft_collection_mismatch(), ); + assert!( + safe::transfer_cap_is_exclusive(&nft_cap), + err::nft_not_exclusive(), + ); let seller = tx_context::sender(ctx); let bids = &mut book.bids; let can_be_filled = !crit_bit::is_empty(bids) && - crit_bit::max_key(bids) <= price; + crit_bit::max_key(bids) >= price; if (can_be_filled) { let highest_bid_price = crit_bit::max_key(bids); @@ -480,7 +462,7 @@ module syrup::orderbook { offer: bid_offer, } = bid; let trade = collection::begin_nft_trade_with(bid_offer, ctx); - let nft = safe::trade_nft(nft_cap, trade, safe); + let nft = safe::trade_nft(nft_cap, trade, safe); transfer(nft, buyer); } else { let id = object::new(ctx); @@ -556,7 +538,7 @@ module syrup::orderbook { let offer = balance::split(coin::balance_mut(wallet), price); let trade = collection::begin_nft_trade_with(offer, ctx); - let nft = safe::trade_nft(nft_cap, trade, safe); + let nft = safe::trade_nft(nft_cap, trade, safe); transfer(nft, buyer); } diff --git a/sources/safe.move b/sources/safe.move index ffb1214..2c29d57 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -4,12 +4,14 @@ module syrup::safe { use sui::object::{ID, UID}; use sui::transfer::transfer_to_object; - use syrup::collection::{CollectionProof, Trade}; + use syrup::collection::{Trade}; /// A shared object for storing NFT's of type `T`, owned by the holder of a unique `OwnerCap`. /// Permissions to allow others to list NFT's can be granted via TransferCap's and BorrowCap's struct Safe has key { id: UID, + owner: address, + // ... contains the fields from MR } /// A unique capability held by the owner of a particular `Safe`. @@ -25,11 +27,10 @@ module syrup::safe { /// the safe with id `safe_id`. Can only be used once. struct TransferCap has key, store { id: UID, - /// The ID of the safe that this capability grants permissions to safe_id: ID, - /// The ID of the NFT that this capability can transfer nft_id: ID, - proof: CollectionProof, + // only one transfer cap for this nft can exist if this is true + is_exlusive: bool, } /// Produce a `TransferCap` for the NFT with `id` in `safe`. @@ -38,13 +39,9 @@ module syrup::safe { abort(0) } - /// Consume `cap`, remove the NFT with `id` from `safe`, and return it to the caller. - /// Requiring `royalty` ensures that the caller has paid the required royalty for this collection - /// before completing the purchase. - /// This invalidates all other `TransferCap`'s by increasing safe.transfer_cap_version - public fun trade_nft( + public fun trade_nft( _cap: TransferCap, - trade: Trade, + trade: Trade, safe: &mut Safe, ): T { transfer_to_object(trade, safe); @@ -52,6 +49,10 @@ module syrup::safe { abort(0) } + public fun safe_owner(safe: &Safe): address { + safe.owner + } + public fun transfer_cap_safe_id(cap: &TransferCap): ID { cap.safe_id } @@ -59,4 +60,8 @@ module syrup::safe { public fun transfer_cap_nft_id(cap: &TransferCap): ID { cap.nft_id } + + public fun transfer_cap_is_exclusive(cap: &TransferCap): bool { + cap.is_exlusive + } } From f982f50d087b634c487a897440e034f9e1197e5f Mon Sep 17 00:00:00 2001 From: porkbrain Date: Thu, 6 Oct 2022 11:19:59 +0200 Subject: [PATCH 05/16] Adding an example foo_nft impl --- Move.toml | 4 ++-- sources/collection.move | 12 ++++++------ sources/crit_bit.move | 2 +- sources/err.move | 2 +- sources/foo_nft.move | 21 +++++++++++++++++++++ sources/orderbook.move | 28 ++++++++++++++-------------- sources/safe.move | 6 +++--- 7 files changed, 48 insertions(+), 27 deletions(-) create mode 100644 sources/foo_nft.move diff --git a/Move.toml b/Move.toml index eed5d3c..a9f1335 100644 --- a/Move.toml +++ b/Move.toml @@ -1,10 +1,10 @@ [package] -name = "Syrup" +name = "LiquidityLayer" version = "0.4.0" [dependencies] Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework", rev = "devnet-0.9.0" } [addresses] -syrup = "0x0" +liquidity_layer = "0x0" # nft_protocol = "0xe6d6f01725d469b7b04d9905e6b3151e8c4264cb" diff --git a/sources/collection.move b/sources/collection.move index 8432907..775b14b 100644 --- a/sources/collection.move +++ b/sources/collection.move @@ -1,7 +1,7 @@ //! Taken from https://github.com/MystenLabs/sui/pull/4887/files#diff-4097e0ffb7703cda3da51b586eba7657dfc6bfe919ca2b1be060aca6d71e8cd2 //! and modified. -module syrup::collection { +module liquidity_layer::collection { use sui::balance::Balance; use sui::object::{Self, ID, UID}; use sui::transfer::transfer_to_object; @@ -16,7 +16,7 @@ module syrup::collection { collection_id: ID } - struct Trade has key { + struct TradeReceipt has key, store { id: UID, } @@ -34,7 +34,7 @@ module syrup::collection { public fun begin_nft_trade_with( amount: Balance, ctx: &mut TxContext, - ): Trade { + ): TradeReceipt { let trade = begin_nft_trade(ctx); pay_for_nft(&mut trade, amount, ctx); @@ -42,14 +42,14 @@ module syrup::collection { } /// Resolve the trade with [`safe::trade_nft`] - public fun begin_nft_trade(ctx: &mut TxContext): Trade { - Trade { + public fun begin_nft_trade(ctx: &mut TxContext): TradeReceipt { + TradeReceipt { id: object::new(ctx), } } public fun pay_for_nft( - trade: &mut Trade, + trade: &mut TradeReceipt, amount: Balance, ctx: &mut TxContext, ) { diff --git a/sources/crit_bit.move b/sources/crit_bit.move index 071f17e..93ab3e9 100644 --- a/sources/crit_bit.move +++ b/sources/crit_bit.move @@ -402,7 +402,7 @@ /// /// --- /// -module syrup::crit_bit { +module liquidity_layer::crit_bit { use std::vector::{ borrow as v_b, borrow_mut as v_b_m, diff --git a/sources/err.move b/sources/err.move index 7dc9ab1..f526d4c 100644 --- a/sources/err.move +++ b/sources/err.move @@ -1,4 +1,4 @@ -module syrup::err { +module liquidity_layer::err { //! Exports error functions. All errors in this smart contract have a prefix //! which distinguishes them from errors in other packages. diff --git a/sources/foo_nft.move b/sources/foo_nft.move new file mode 100644 index 0000000..6441d6f --- /dev/null +++ b/sources/foo_nft.move @@ -0,0 +1,21 @@ +module liquidity_layer::foo_nft { + use sui::object::UID; + use liquidity_layer::collection::{TradeReceipt, TradePayment}; + use liquidity_layer::safe::Safe; + + struct Witness {} + + /// The NFT itself. + struct Foo has key, store { + id: UID, + // ... + } + + public fun collect_fees( + _receipt: TradeReceipt, + _payment: TradePayment, + _safe: &mut Safe, + ) { + abort(0) + } +} diff --git a/sources/orderbook.move b/sources/orderbook.move index c031f27..00e40e4 100644 --- a/sources/orderbook.move +++ b/sources/orderbook.move @@ -1,4 +1,4 @@ -module syrup::orderbook { +module liquidity_layer::orderbook { //! Orderbook where bids are fungible tokens and asks are NFTs. //! A bid is a request to buy one NFT from a specific collection. //! An ask is one NFT with a min price condition. @@ -22,10 +22,10 @@ module syrup::orderbook { use sui::object::{Self, ID, UID}; use sui::transfer::transfer; use sui::tx_context::{Self, TxContext}; - use syrup::collection; - use syrup::crit_bit::{Self, CB as CBTree}; - use syrup::err; - use syrup::safe::{Self, Safe, TransferCap}; + use liquidity_layer::collection; + use liquidity_layer::crit_bit::{Self, CB as CBTree}; + use liquidity_layer::err; + use liquidity_layer::safe::{Self, Safe, TransferCap}; /// A critbit order book implementation. Contains two ordered trees: /// 1. bids ASC @@ -105,7 +105,7 @@ module syrup::orderbook { /// If the `price` is higher than the lowest ask requested price, then we /// execute a trade straight away. Otherwise we add the bid to the /// orderbook's state. - public entry fun create_bid( + public entry fun create_bid( book: &mut Orderbook, price: u64, wallet: &mut Coin, @@ -115,7 +115,7 @@ module syrup::orderbook { assert!(book.protected_actions.create_bid, err::action_not_public()); create_bid_(book, price, wallet, safe, ctx) } - public fun create_bid_protected( + public fun create_bid_protected( _witness: Wness, book: &mut Orderbook, price: u64, @@ -151,7 +151,7 @@ module syrup::orderbook { /// there exists a bid with higher offer than `requsted_tokens`, then trade /// is immeidately executed. Otherwise the NFT is transferred to a newly /// created ask object and the object is inserted to the orderbook. - public entry fun create_ask( + public entry fun create_ask( book: &mut Orderbook, requsted_tokens: u64, nft_cap: TransferCap, @@ -161,7 +161,7 @@ module syrup::orderbook { assert!(book.protected_actions.create_ask, err::action_not_public()); create_ask_(book, requsted_tokens, nft_cap, safe, ctx) } - public fun create_ask_protected( + public fun create_ask_protected( _witness: Wness, book: &mut Orderbook, requsted_tokens: u64, @@ -199,7 +199,7 @@ module syrup::orderbook { /// Buys a specific NFT from the orderbook. This is an atypical OB API as /// with fungible tokens, you just want to get the cheapest ask. /// However, with NFTs, you might want to get a specific one. - public entry fun buy_nft( + public entry fun buy_nft( book: &mut Orderbook, nft_id: ID, price: u64, @@ -210,7 +210,7 @@ module syrup::orderbook { assert!(book.protected_actions.buy_nft, err::action_not_public()); buy_nft_(book, nft_id, price, wallet, safe, ctx) } - public entry fun buy_nft_protected( + public entry fun buy_nft_protected( _witness: Wness, book: &mut Orderbook, nft_id: ID, @@ -320,7 +320,7 @@ module syrup::orderbook { } } - fun create_bid_( + fun create_bid_( book: &mut Orderbook, price: u64, wallet: &mut Coin, @@ -420,7 +420,7 @@ module syrup::orderbook { ); } - fun create_ask_( + fun create_ask_( book: &mut Orderbook, price: u64, nft_cap: TransferCap, @@ -513,7 +513,7 @@ module syrup::orderbook { transfer(nft_cap, sender); } - fun buy_nft_( + fun buy_nft_( book: &mut Orderbook, nft_id: ID, price: u64, diff --git a/sources/safe.move b/sources/safe.move index 2c29d57..0180d5b 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -1,10 +1,10 @@ //! Taken from https://github.com/MystenLabs/sui/pull/4887/files#diff-96b0dc07cabd79292618c993cd473c43ca81cd2f742266014967cdea1a7c6186 //! and modified. -module syrup::safe { +module liquidity_layer::safe { use sui::object::{ID, UID}; use sui::transfer::transfer_to_object; - use syrup::collection::{Trade}; + use liquidity_layer::collection::{TradeReceipt}; /// A shared object for storing NFT's of type `T`, owned by the holder of a unique `OwnerCap`. /// Permissions to allow others to list NFT's can be granted via TransferCap's and BorrowCap's @@ -41,7 +41,7 @@ module syrup::safe { public fun trade_nft( _cap: TransferCap, - trade: Trade, + trade: TradeReceipt, safe: &mut Safe, ): T { transfer_to_object(trade, safe); From fd75c4cc687e4718ed1cf94b0de9c2b747f3c147 Mon Sep 17 00:00:00 2001 From: porkbrain Date: Thu, 6 Oct 2022 11:20:52 +0200 Subject: [PATCH 06/16] Removing nft protocol --- Move.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Move.toml b/Move.toml index a9f1335..65b2af7 100644 --- a/Move.toml +++ b/Move.toml @@ -7,4 +7,3 @@ Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-fram [addresses] liquidity_layer = "0x0" -# nft_protocol = "0xe6d6f01725d469b7b04d9905e6b3151e8c4264cb" From ad2c5597c7b06413f37fd6c91c1685f289e449d7 Mon Sep 17 00:00:00 2001 From: porkbrain Date: Thu, 6 Oct 2022 11:21:50 +0200 Subject: [PATCH 07/16] Renaming function in foo_nft --- sources/foo_nft.move | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/foo_nft.move b/sources/foo_nft.move index 6441d6f..4435a4d 100644 --- a/sources/foo_nft.move +++ b/sources/foo_nft.move @@ -11,7 +11,7 @@ module liquidity_layer::foo_nft { // ... } - public fun collect_fees( + public fun collect_royalty( _receipt: TradeReceipt, _payment: TradePayment, _safe: &mut Safe, From 26a3e9c1aa23b95ce02f34e9e46e2821488688a9 Mon Sep 17 00:00:00 2001 From: porkbrain Date: Thu, 6 Oct 2022 16:19:33 +0200 Subject: [PATCH 08/16] Updating README with Safe --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0d3f872..49d3f83 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ - Sui v0.9.0 +Working title: _syrup_ (to be changed) + NFT liquidity layer is a suite of modules which aim to bring NFT trading across marketplaces to a single point, thereby concentrating the liquidity into a common interface. @@ -18,16 +20,12 @@ An ask is one NFT with a min price condition. `Ask` is an object which is associated with a single NFT. When `Ask` is created, we transfer the ownership of the NFT to this -new object. -When an `Ask` is matched with a bid, we transfer the ownership of the -`ask` object to the bid owner (buyer). -The buyer can then claim the NFT via `claim_nft` endpoint. +new object. To be more precise, we transfer the `safe::TransferCap`. One can: - create a new orderbook between a given collection and a BID token (witness pattern protected) -- collect fees (witness pattern protected) - set publicly accessible actions to be witness protected - open a new BID - cancel an existing BID they own From fec3d5989a18d6b6d9a2aacab39d9aa7292dae97 Mon Sep 17 00:00:00 2001 From: porkbrain Date: Thu, 6 Oct 2022 18:44:57 +0200 Subject: [PATCH 09/16] Expanding on the idea of TradePayment --- sources/collection.move | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sources/collection.move b/sources/collection.move index 775b14b..645736e 100644 --- a/sources/collection.move +++ b/sources/collection.move @@ -22,10 +22,11 @@ module liquidity_layer::collection { struct TradePayment has key { id: UID, - /// Same as `Trade::id` if the NFT was sold for just one FT kind. - /// However, can bind multiple `Trade` object with a different generic - /// into one logical unit if the royalty logic requires e.g. payment in - /// both `SUI` and `USDC`. + /// Could be same as `TradePayment::id` if the NFT was sold for just one + /// FT kind. + /// However, can bind multiple `TradePayment` objects with a different + /// generic into one logical unit if the royalty logic requires e.g. + /// payment in both `SUI` and `USDC`. trade: ID, amount: Balance, } From d8f9f20691fb82fe54d6a470fe549d0bd3bd8b18 Mon Sep 17 00:00:00 2001 From: porkbrain Date: Thu, 6 Oct 2022 18:56:42 +0200 Subject: [PATCH 10/16] Posting a link to complication with Safe --- sources/orderbook.move | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sources/orderbook.move b/sources/orderbook.move index 00e40e4..b41e276 100644 --- a/sources/orderbook.move +++ b/sources/orderbook.move @@ -109,7 +109,7 @@ module liquidity_layer::orderbook { book: &mut Orderbook, price: u64, wallet: &mut Coin, - safe: &mut Safe, // !!! + safe: &mut Safe, // !!! https://github.com/MystenLabs/sui/pull/4887/files#r984862924 ctx: &mut TxContext, ) { assert!(book.protected_actions.create_bid, err::action_not_public()); @@ -120,7 +120,7 @@ module liquidity_layer::orderbook { book: &mut Orderbook, price: u64, wallet: &mut Coin, - safe: &mut Safe, // !!! + safe: &mut Safe, // !!! https://github.com/MystenLabs/sui/pull/4887/files#r984862924 ctx: &mut TxContext, ) { create_bid_(book, price, wallet, safe, ctx) @@ -324,7 +324,7 @@ module liquidity_layer::orderbook { book: &mut Orderbook, price: u64, wallet: &mut Coin, - safe: &mut Safe, // !!! + safe: &mut Safe, // !!! https://github.com/MystenLabs/sui/pull/4887/files#r984862924 ctx: &mut TxContext, ) { let buyer = tx_context::sender(ctx); From d19fd88647234d956974d922a9d702b196b4c187 Mon Sep 17 00:00:00 2001 From: porkbrain Date: Mon, 10 Oct 2022 18:46:12 +0200 Subject: [PATCH 11/16] Using TradeIntermediate abstraction --- sources/collection.move | 5 - sources/orderbook.move | 234 ++++++++++++++++++++++++---------------- sources/safe.move | 18 ++-- 3 files changed, 152 insertions(+), 105 deletions(-) diff --git a/sources/collection.move b/sources/collection.move index 645736e..598d333 100644 --- a/sources/collection.move +++ b/sources/collection.move @@ -11,11 +11,6 @@ module liquidity_layer::collection { id: UID, } - /// Proof that the given NFT is one of the limited `total_supply` NFT's in `Collection` - struct CollectionProof has store { - collection_id: ID - } - struct TradeReceipt has key, store { id: UID, } diff --git a/sources/orderbook.move b/sources/orderbook.move index b41e276..895ac47 100644 --- a/sources/orderbook.move +++ b/sources/orderbook.move @@ -16,21 +16,22 @@ module liquidity_layer::orderbook { // TODO: collect commision on trade // TODO: protocol toll + use liquidity_layer::collection; + use liquidity_layer::crit_bit::{Self, CB as CBTree}; + use liquidity_layer::err; + use liquidity_layer::safe::{Self, Safe, ExclusiveTransferCap}; + use std::option::{Self, Option}; use std::vector; use sui::balance::{Self, Balance}; use sui::coin::{Self, Coin}; use sui::object::{Self, ID, UID}; - use sui::transfer::transfer; + use sui::transfer::{transfer, share_object}; use sui::tx_context::{Self, TxContext}; - use liquidity_layer::collection; - use liquidity_layer::crit_bit::{Self, CB as CBTree}; - use liquidity_layer::err; - use liquidity_layer::safe::{Self, Safe, TransferCap}; /// A critbit order book implementation. Contains two ordered trees: /// 1. bids ASC /// 2. asks DESC - struct Orderbook has key { + struct Orderbook has key { id: UID, /// Actions which have a flag set to true can only be called via a /// witness protected implementation. @@ -93,11 +94,25 @@ module liquidity_layer::orderbook { /// How many tokens does the seller want for their NFT in exchange. price: u64, /// Capability to get an NFT from a safe. - nft_cap: TransferCap, + nft_cap: ExclusiveTransferCap, /// Who owns the NFT. owner: address, } + /// We cannot just give the `ExclusiveTransferCap` to the buyer. They could + /// decide to burn the NFT with their funds by never going to the + /// `Safe` object and finishing the trade, but rather keeping the NFT's + /// `ExlusiveTransferCap`. + /// + /// Therefore `TradeIntermediate` is made a share object and can be called + /// permissionlessly. + struct TradeIntermediate has key { + id: UID, + nft_cap: Option, + buyer: address, + paid: Balance, + } + /// How many (`price`) fungible tokens should be taken from sender's wallet /// and put into the orderbook with the intention of exchanging them for /// 1 NFT. @@ -105,31 +120,29 @@ module liquidity_layer::orderbook { /// If the `price` is higher than the lowest ask requested price, then we /// execute a trade straight away. Otherwise we add the bid to the /// orderbook's state. - public entry fun create_bid( - book: &mut Orderbook, + public entry fun create_bid( + book: &mut Orderbook, price: u64, wallet: &mut Coin, - safe: &mut Safe, // !!! https://github.com/MystenLabs/sui/pull/4887/files#r984862924 ctx: &mut TxContext, ) { assert!(book.protected_actions.create_bid, err::action_not_public()); - create_bid_(book, price, wallet, safe, ctx) + create_bid_(book, price, wallet, ctx) } - public fun create_bid_protected( - _witness: Wness, - book: &mut Orderbook, + public fun create_bid_protected( + _witness: W, + book: &mut Orderbook, price: u64, wallet: &mut Coin, - safe: &mut Safe, // !!! https://github.com/MystenLabs/sui/pull/4887/files#r984862924 ctx: &mut TxContext, ) { - create_bid_(book, price, wallet, safe, ctx) + create_bid_(book, price, wallet, ctx) } /// Cancel a bid owned by the sender at given price. If there are two bids /// with the same price, the one created later is cancelled. - public entry fun cancel_bid( - book: &mut Orderbook, + public entry fun cancel_bid( + book: &mut Orderbook, requested_bid_offer_to_cancel: u64, wallet: &mut Coin, ctx: &mut TxContext, @@ -137,9 +150,9 @@ module liquidity_layer::orderbook { assert!(book.protected_actions.cancel_bid, err::action_not_public()); cancel_bid_(book, requested_bid_offer_to_cancel, wallet, ctx) } - public fun cancel_bid_protected( - _witness: Wness, - book: &mut Orderbook, + public fun cancel_bid_protected( + _witness: W, + book: &mut Orderbook, requested_bid_offer_to_cancel: u64, wallet: &mut Coin, ctx: &mut TxContext, @@ -151,21 +164,21 @@ module liquidity_layer::orderbook { /// there exists a bid with higher offer than `requsted_tokens`, then trade /// is immeidately executed. Otherwise the NFT is transferred to a newly /// created ask object and the object is inserted to the orderbook. - public entry fun create_ask( - book: &mut Orderbook, + public entry fun create_ask( + book: &mut Orderbook, requsted_tokens: u64, - nft_cap: TransferCap, + nft_cap: ExclusiveTransferCap, safe: &mut Safe, ctx: &mut TxContext, ) { assert!(book.protected_actions.create_ask, err::action_not_public()); create_ask_(book, requsted_tokens, nft_cap, safe, ctx) } - public fun create_ask_protected( - _witness: Wness, - book: &mut Orderbook, + public fun create_ask_protected( + _witness: W, + book: &mut Orderbook, requsted_tokens: u64, - nft_cap: TransferCap, + nft_cap: ExclusiveTransferCap, safe: &mut Safe, ctx: &mut TxContext, ) { @@ -177,8 +190,8 @@ module liquidity_layer::orderbook { /// /// This API might be improved in future as we use a different data /// structure for the orderbook. - public entry fun cancel_ask( - book: &mut Orderbook, + public entry fun cancel_ask( + book: &mut Orderbook, nft_price: u64, nft_id: ID, ctx: &mut TxContext, @@ -186,9 +199,9 @@ module liquidity_layer::orderbook { assert!(book.protected_actions.cancel_ask, err::action_not_public()); cancel_ask_(book, nft_price, nft_id, ctx) } - public entry fun cancel_ask_protected( - _witness: Wness, - book: &mut Orderbook, + public entry fun cancel_ask_protected( + _witness: W, + book: &mut Orderbook, nft_price: u64, nft_id: ID, ctx: &mut TxContext, @@ -199,8 +212,8 @@ module liquidity_layer::orderbook { /// Buys a specific NFT from the orderbook. This is an atypical OB API as /// with fungible tokens, you just want to get the cheapest ask. /// However, with NFTs, you might want to get a specific one. - public entry fun buy_nft( - book: &mut Orderbook, + public entry fun buy_nft( + book: &mut Orderbook, nft_id: ID, price: u64, wallet: &mut Coin, @@ -210,9 +223,9 @@ module liquidity_layer::orderbook { assert!(book.protected_actions.buy_nft, err::action_not_public()); buy_nft_(book, nft_id, price, wallet, safe, ctx) } - public entry fun buy_nft_protected( - _witness: Wness, - book: &mut Orderbook, + public entry fun buy_nft_protected( + _witness: W, + book: &mut Orderbook, nft_id: ID, price: u64, wallet: &mut Coin, @@ -231,51 +244,64 @@ module liquidity_layer::orderbook { /// To implement specific logic in your smart contract, you can toggle the /// protection on specific actions. That will make them only accessible via /// witness protected methods. - public fun create( - _witness: Wness, + public fun create( + _witness: W, + ctx: &mut TxContext, + ): Orderbook { + create_(no_protection(), ctx) + } + + /// When a bid is created and there's an ask with a lower price, then the + /// trade cannot be resolved immiedately. + /// That's because we don't know the `Safe` ID up front in OB. + /// Therefore, the tx creates `TradeIntermediate` which then has to be + /// permission-lessly resolved via this endpoint. + public fun finish_trade( + trade: &mut TradeIntermediate, + safe: &mut Safe, ctx: &mut TxContext, - ): Orderbook { - create_(no_protection(), ctx) + ) { + finish_trade_(trade, safe, ctx) } - public fun toggle_protection_on_buy_nft( - _witness: Wness, - book: &mut Orderbook, + public fun toggle_protection_on_buy_nft( + _witness: W, + book: &mut Orderbook, ) { book.protected_actions.buy_nft = !book.protected_actions.buy_nft; } - public fun toggle_protection_on_cancel_ask( - _witness: Wness, - book: &mut Orderbook, + public fun toggle_protection_on_cancel_ask( + _witness: W, + book: &mut Orderbook, ) { book.protected_actions.cancel_ask = !book.protected_actions.cancel_ask; } - public fun toggle_protection_on_cancel_bid( - _witness: Wness, - book: &mut Orderbook, + public fun toggle_protection_on_cancel_bid( + _witness: W, + book: &mut Orderbook, ) { book.protected_actions.cancel_bid = !book.protected_actions.cancel_bid; } - public fun toggle_protection_on_create_ask( - _witness: Wness, - book: &mut Orderbook, + public fun toggle_protection_on_create_ask( + _witness: W, + book: &mut Orderbook, ) { book.protected_actions.create_ask = !book.protected_actions.create_ask; } - public fun toggle_protection_on_create_bid( - _witness: Wness, - book: &mut Orderbook, + public fun toggle_protection_on_create_bid( + _witness: W, + book: &mut Orderbook, ) { book.protected_actions.create_bid = !book.protected_actions.create_bid; } - public fun borrow_bids( - book: &Orderbook, + public fun borrow_bids( + book: &Orderbook, ): &CBTree>> { &book.bids } @@ -288,8 +314,8 @@ module liquidity_layer::orderbook { bid.owner } - public fun borrow_asks( - book: &Orderbook, + public fun borrow_asks( + book: &Orderbook, ): &CBTree> { &book.asks } @@ -298,7 +324,7 @@ module liquidity_layer::orderbook { ask.price } - public fun ask_nft(ask: &Ask): &TransferCap { + public fun ask_nft(ask: &Ask): &ExclusiveTransferCap { &ask.nft_cap } @@ -306,13 +332,13 @@ module liquidity_layer::orderbook { ask.owner } - fun create_( + fun create_( protected_actions: WitnessProtectedActions, ctx: &mut TxContext, - ): Orderbook { + ): Orderbook { let id = object::new(ctx); - Orderbook { + Orderbook { id, protected_actions, asks: crit_bit::empty(), @@ -320,11 +346,10 @@ module liquidity_layer::orderbook { } } - fun create_bid_( - book: &mut Orderbook, + fun create_bid_( + book: &mut Orderbook, price: u64, wallet: &mut Coin, - safe: &mut Safe, // !!! https://github.com/MystenLabs/sui/pull/4887/files#r984862924 ctx: &mut TxContext, ) { let buyer = tx_context::sender(ctx); @@ -339,7 +364,8 @@ module liquidity_layer::orderbook { crit_bit::min_key(asks) <= price; if (can_be_filled) { - let lowest_ask_price = crit_bit::min_key(asks); // TODO: recomputed + // OPTIMIZE: this is being recomputed + let lowest_ask_price = crit_bit::min_key(asks); let price_level = crit_bit::borrow_mut(asks, lowest_ask_price); let ask = vector::remove( @@ -355,14 +381,18 @@ module liquidity_layer::orderbook { let Ask { id, price: _, - owner: seller, + owner: _, nft_cap, } = ask; - assert!(safe::safe_owner(safe) == seller, 0); - let trade = collection::begin_nft_trade_with(bid_offer, ctx); - let nft = safe::trade_nft(nft_cap, trade, safe); - transfer(nft, buyer); object::delete(id); + + // see also `finish_trade` entry point + share_object(TradeIntermediate { + id: object::new(ctx), + nft_cap: option::some(nft_cap), + buyer, + paid: bid_offer, + }); } else { let order = Bid { offer: bid_offer, owner: buyer }; @@ -381,8 +411,8 @@ module liquidity_layer::orderbook { } } - fun cancel_bid_( - book: &mut Orderbook, + fun cancel_bid_( + book: &mut Orderbook, requested_bid_offer_to_cancel: u64, wallet: &mut Coin, ctx: &mut TxContext, @@ -420,21 +450,17 @@ module liquidity_layer::orderbook { ); } - fun create_ask_( - book: &mut Orderbook, + fun create_ask_( + book: &mut Orderbook, price: u64, - nft_cap: TransferCap, + nft_cap: ExclusiveTransferCap, safe: &mut Safe, ctx: &mut TxContext, ) { assert!( - object::id(safe) == safe::transfer_cap_safe_id(&nft_cap), + object::id(safe) == safe::exclusive_transfer_cap_safe_id(&nft_cap), err::nft_collection_mismatch(), ); - assert!( - safe::transfer_cap_is_exclusive(&nft_cap), - err::nft_not_exclusive(), - ); let seller = tx_context::sender(ctx); @@ -462,7 +488,7 @@ module liquidity_layer::orderbook { offer: bid_offer, } = bid; let trade = collection::begin_nft_trade_with(bid_offer, ctx); - let nft = safe::trade_nft(nft_cap, trade, safe); + let nft = safe::trade_nft(nft_cap, trade, safe); transfer(nft, buyer); } else { let id = object::new(ctx); @@ -488,8 +514,8 @@ module liquidity_layer::orderbook { } } - fun cancel_ask_( - book: &mut Orderbook, + fun cancel_ask_( + book: &mut Orderbook, nft_price: u64, nft_id: ID, ctx: &mut TxContext, @@ -513,8 +539,8 @@ module liquidity_layer::orderbook { transfer(nft_cap, sender); } - fun buy_nft_( - book: &mut Orderbook, + fun buy_nft_( + book: &mut Orderbook, nft_id: ID, price: u64, wallet: &mut Coin, @@ -538,10 +564,36 @@ module liquidity_layer::orderbook { let offer = balance::split(coin::balance_mut(wallet), price); let trade = collection::begin_nft_trade_with(offer, ctx); - let nft = safe::trade_nft(nft_cap, trade, safe); + let nft = safe::trade_nft(nft_cap, trade, safe); transfer(nft, buyer); } + fun finish_trade_( + trade: &mut TradeIntermediate, + safe: &mut Safe, + ctx: &mut TxContext, + ) { + let TradeIntermediate { + id: _, + nft_cap, + paid, + buyer, + } = trade; + + let amount = balance::value(paid); + let nft_cap = option::extract(nft_cap); + + assert!( + object::id(safe) == safe::exclusive_transfer_cap_safe_id(&nft_cap), + err::nft_collection_mismatch(), + ); + + let trade = + collection::begin_nft_trade_with(balance::split(paid, amount), ctx); + let nft = safe::trade_nft(nft_cap, trade, safe); + transfer(nft, *buyer); + } + /// 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 { @@ -557,7 +609,7 @@ module liquidity_layer::orderbook { while (asks_count > index) { let ask = vector::borrow(price_level, index); // on the same price level, we search for the specified NFT - if (nft_id == safe::transfer_cap_nft_id(&ask.nft_cap)) { + if (nft_id == safe::exclusive_transfer_cap_nft_id(&ask.nft_cap)) { break }; diff --git a/sources/safe.move b/sources/safe.move index 0180d5b..ba07e6e 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -29,8 +29,12 @@ module liquidity_layer::safe { id: UID, safe_id: ID, nft_id: ID, - // only one transfer cap for this nft can exist if this is true - is_exlusive: bool, + } + + struct ExclusiveTransferCap has key, store { + id: UID, + safe_id: ID, + nft_id: ID, } /// Produce a `TransferCap` for the NFT with `id` in `safe`. @@ -40,7 +44,7 @@ module liquidity_layer::safe { } public fun trade_nft( - _cap: TransferCap, + _cap: ExclusiveTransferCap, trade: TradeReceipt, safe: &mut Safe, ): T { @@ -53,15 +57,11 @@ module liquidity_layer::safe { safe.owner } - public fun transfer_cap_safe_id(cap: &TransferCap): ID { + public fun exclusive_transfer_cap_safe_id(cap: &ExclusiveTransferCap): ID { cap.safe_id } - public fun transfer_cap_nft_id(cap: &TransferCap): ID { + public fun exclusive_transfer_cap_nft_id(cap: &ExclusiveTransferCap): ID { cap.nft_id } - - public fun transfer_cap_is_exclusive(cap: &TransferCap): bool { - cap.is_exlusive - } } From 6594b654f93a648e7fc7f792b07fe7b2faefc46d Mon Sep 17 00:00:00 2001 From: porkbrain Date: Tue, 11 Oct 2022 15:19:32 +0200 Subject: [PATCH 12/16] Showcasing commissions --- sources/collection.move | 39 ++++--- sources/foo_nft.move | 3 +- sources/orderbook.move | 239 +++++++++++++++++++++++++++++++++++++--- sources/safe.move | 4 +- 4 files changed, 249 insertions(+), 36 deletions(-) diff --git a/sources/collection.move b/sources/collection.move index 598d333..02eb3c6 100644 --- a/sources/collection.move +++ b/sources/collection.move @@ -1,59 +1,70 @@ //! Taken from https://github.com/MystenLabs/sui/pull/4887/files#diff-4097e0ffb7703cda3da51b586eba7657dfc6bfe919ca2b1be060aca6d71e8cd2 //! and modified. +//! +//! This will eventually be part of a different package. module liquidity_layer::collection { use sui::balance::Balance; use sui::object::{Self, ID, UID}; use sui::transfer::transfer_to_object; use sui::tx_context::TxContext; + use sui::vec_set::{Self, VecSet}; struct Collection has key, store { id: UID, } - struct TradeReceipt has key, store { + struct TradeReceipt has key, store { id: UID, + /// Each trade is done with one of more payments. + /// + /// IDs of `TradePayment` child objects of this receipt. + payments: VecSet, } struct TradePayment has key { id: UID, - /// Could be same as `TradePayment::id` if the NFT was sold for just one - /// FT kind. - /// However, can bind multiple `TradePayment` objects with a different - /// generic into one logical unit if the royalty logic requires e.g. - /// payment in both `SUI` and `USDC`. - trade: ID, + trade_receipt: ID, amount: Balance, + /// The address where the amount should be transferred to. + /// This could be either the payment for the seller or a marketplace's + /// commision. + beneficiary: address, } /// Resolve the trade with [`safe::trade_nft`] - public fun begin_nft_trade_with( + public fun begin_nft_trade_with( amount: Balance, + beneficiary: address, ctx: &mut TxContext, - ): TradeReceipt { + ): TradeReceipt { let trade = begin_nft_trade(ctx); - pay_for_nft(&mut trade, amount, ctx); + pay_for_nft(&mut trade, amount, beneficiary, ctx); trade } /// Resolve the trade with [`safe::trade_nft`] - public fun begin_nft_trade(ctx: &mut TxContext): TradeReceipt { + public fun begin_nft_trade(ctx: &mut TxContext): TradeReceipt { TradeReceipt { id: object::new(ctx), + payments: vec_set::empty(), } } - public fun pay_for_nft( - trade: &mut TradeReceipt, + public fun pay_for_nft( + trade: &mut TradeReceipt, amount: Balance, + beneficiary: address, ctx: &mut TxContext, ) { let payment = TradePayment { id: object::new(ctx), - trade: object::id(trade), + trade_receipt: object::id(trade), amount, + beneficiary, }; + vec_set::insert(&mut trade.payments, object::id(&payment)); transfer_to_object(payment, trade); } } diff --git a/sources/foo_nft.move b/sources/foo_nft.move index 4435a4d..fedcab8 100644 --- a/sources/foo_nft.move +++ b/sources/foo_nft.move @@ -1,6 +1,6 @@ module liquidity_layer::foo_nft { use sui::object::UID; - use liquidity_layer::collection::{TradeReceipt, TradePayment}; + use liquidity_layer::collection::{TradeReceipt}; use liquidity_layer::safe::Safe; struct Witness {} @@ -13,7 +13,6 @@ module liquidity_layer::foo_nft { public fun collect_royalty( _receipt: TradeReceipt, - _payment: TradePayment, _safe: &mut Safe, ) { abort(0) diff --git a/sources/orderbook.move b/sources/orderbook.move index 895ac47..49d9116 100644 --- a/sources/orderbook.move +++ b/sources/orderbook.move @@ -11,12 +11,12 @@ module liquidity_layer::orderbook { //! - cancel an existing bid they own; //! - offer an NFT if collection matches OB collection; //! - cancel an existing NFT offer; - //! - instantly buy an specific NFT. + //! - instantly buy an specific NFT; + //! - open bids and asks with a commission on behalf of a user. - // TODO: collect commision on trade // TODO: protocol toll - use liquidity_layer::collection; + use liquidity_layer::collection::{Self, TradeReceipt}; use liquidity_layer::crit_bit::{Self, CB as CBTree}; use liquidity_layer::err; use liquidity_layer::safe::{Self, Safe, ExclusiveTransferCap}; @@ -74,12 +74,25 @@ module liquidity_layer::orderbook { } /// An offer for a single NFT in a collection. - struct Bid has store { + struct Bid has store { /// How many "T"okens are being offered by the order issuer for one NFT. - offer: Balance, + offer: Balance, /// The address of the user who created this bid and who will receive an /// NFT in exchange for their tokens. owner: address, + /// If the NFT is offered via a marketplace or a wallet, the + /// faciliatator can optionally set how many tokens they want to claim + /// on top of the + commission: Option>, + } + /// Enables collection of wallet/marketplace collection for buying NFTs. + /// 1. user bids via wallet to buy NFT for `p`, wallet wants fee `f` + /// 2. when executed, `p` goes to seller and `f` goes to wallet + struct BidCommission has store { + /// This is given to the facilitator of the trade. + cut: Balance, + /// A new `Coin` object is created and sent to this address. + beneficiary: address, } /// Object which is associated with a single NFT. @@ -97,6 +110,22 @@ module liquidity_layer::orderbook { nft_cap: ExclusiveTransferCap, /// Who owns the NFT. owner: address, + /// If the NFT is offered via a marketplace or a wallet, the + /// faciliatator can optionally set how many tokens they want to claim + /// from the price of the NFT for themselves as a commission. + commission: Option, + } + /// Enables collection of wallet/marketplace collection for listing an NFT. + /// 1. user lists NFT via wallet for price `p`, wallet requests fee `f` + /// 2. when executed, `p - f` goes to user and `f` goes to wallet + struct AskCommission has store, drop { + /// How many tokens of the transferred amount should go to the party + /// which holds the private key of `beneficiary` address. + /// + /// Always less than ask price. + cut: u64, + /// A new `Coin` object is created and sent to this address. + beneficiary: address, } /// We cannot just give the `ExclusiveTransferCap` to the buyer. They could @@ -111,6 +140,7 @@ module liquidity_layer::orderbook { nft_cap: Option, buyer: address, paid: Balance, + commission: Option, } /// How many (`price`) fungible tokens should be taken from sender's wallet @@ -127,7 +157,7 @@ module liquidity_layer::orderbook { ctx: &mut TxContext, ) { assert!(book.protected_actions.create_bid, err::action_not_public()); - create_bid_(book, price, wallet, ctx) + create_bid_(book, price, option::none(), wallet, ctx) } public fun create_bid_protected( _witness: W, @@ -136,7 +166,37 @@ module liquidity_layer::orderbook { wallet: &mut Coin, ctx: &mut TxContext, ) { - create_bid_(book, price, wallet, ctx) + create_bid_(book, price, option::none(), wallet, ctx) + } + public entry fun create_bid_with_commission( + book: &mut Orderbook, + price: u64, + beneficiary: address, + commission_ft: u64, + wallet: &mut Coin, + ctx: &mut TxContext, + ) { + assert!(book.protected_actions.create_bid, err::action_not_public()); + let commission = BidCommission { + beneficiary, + cut: balance::split(coin::balance_mut(wallet), commission_ft), + }; + create_bid_(book, price, option::some(commission), wallet, ctx) + } + public fun create_bid_with_commission_protected( + _witness: W, + book: &mut Orderbook, + price: u64, + beneficiary: address, + commission_ft: u64, + wallet: &mut Coin, + ctx: &mut TxContext, + ) { + let commission = BidCommission { + beneficiary, + cut: balance::split(coin::balance_mut(wallet), commission_ft), + }; + create_bid_(book, price, option::some(commission), wallet, ctx) } /// Cancel a bid owned by the sender at given price. If there are two bids @@ -172,7 +232,7 @@ module liquidity_layer::orderbook { ctx: &mut TxContext, ) { assert!(book.protected_actions.create_ask, err::action_not_public()); - create_ask_(book, requsted_tokens, nft_cap, safe, ctx) + create_ask_(book, requsted_tokens, option::none(), nft_cap, safe, ctx) } public fun create_ask_protected( _witness: W, @@ -182,7 +242,43 @@ module liquidity_layer::orderbook { safe: &mut Safe, ctx: &mut TxContext, ) { - create_ask_(book, requsted_tokens, nft_cap, safe, ctx) + create_ask_(book, requsted_tokens, option::none(), nft_cap, safe, ctx) + } + public entry fun create_ask_with_commission( + book: &mut Orderbook, + requsted_tokens: u64, + nft_cap: ExclusiveTransferCap, + safe: &mut Safe, + beneficiary: address, + commission: u64, + ctx: &mut TxContext, + ) { + assert!(book.protected_actions.create_ask, err::action_not_public()); + let commission = AskCommission { + cut: commission, + beneficiary, + }; + create_ask_( + book, requsted_tokens, option::some(commission), nft_cap, safe, ctx + ) + } + public fun create_ask_with_commission_protected( + _witness: W, + book: &mut Orderbook, + requsted_tokens: u64, + nft_cap: ExclusiveTransferCap, + safe: &mut Safe, + beneficiary: address, + commission: u64, + ctx: &mut TxContext, + ) { + let commission = AskCommission { + cut: commission, + beneficiary, + }; + create_ask_( + book, requsted_tokens, option::some(commission), nft_cap, safe, ctx + ) } /// We could remove the NFT requested price from the argument, but then the @@ -349,6 +445,7 @@ module liquidity_layer::orderbook { fun create_bid_( book: &mut Orderbook, price: u64, + bid_commission: Option>, wallet: &mut Coin, ctx: &mut TxContext, ) { @@ -383,6 +480,7 @@ module liquidity_layer::orderbook { price: _, owner: _, nft_cap, + commission: ask_commission, } = ask; object::delete(id); @@ -390,11 +488,18 @@ module liquidity_layer::orderbook { share_object(TradeIntermediate { id: object::new(ctx), nft_cap: option::some(nft_cap), + commission: ask_commission, buyer, paid: bid_offer, }); + + transfer_bid_commission(bid_commission, ctx); } else { - let order = Bid { offer: bid_offer, owner: buyer }; + let order = Bid { + offer: bid_offer, + owner: buyer, + commission: bid_commission, + }; if (crit_bit::has_key(&book.bids, price)) { vector::push_back( @@ -442,17 +547,29 @@ module liquidity_layer::orderbook { assert!(index < bids_count, err::order_owner_must_be_sender()); - let Bid { offer, owner: _owner } = vector::remove(price_level, index); + let Bid { offer, owner: _owner, commission } = + vector::remove(price_level, index); balance::join( coin::balance_mut(wallet), offer, ); + + if (option::is_some(&commission)) { + let BidCommission { cut, beneficiary: _ } = + option::extract(&mut commission); + balance::join( + coin::balance_mut(wallet), + cut, + ); + }; + option::destroy_none(commission); } fun create_ask_( book: &mut Orderbook, price: u64, + ask_commission: Option, nft_cap: ExclusiveTransferCap, safe: &mut Safe, ctx: &mut TxContext, @@ -486,10 +603,22 @@ module liquidity_layer::orderbook { let Bid { owner: buyer, offer: bid_offer, + commission: bid_commission, } = bid; - let trade = collection::begin_nft_trade_with(bid_offer, ctx); + + let trade = create_nft_trade_receipt( + &mut bid_offer, + buyer, + &mut ask_commission, + ctx, + ); + option::destroy_none(ask_commission); + balance::destroy_zero(bid_offer); + let nft = safe::trade_nft(nft_cap, trade, safe); transfer(nft, buyer); + + transfer_bid_commission(bid_commission, ctx); } else { let id = object::new(ctx); let ask = Ask { @@ -497,6 +626,7 @@ module liquidity_layer::orderbook { price, owner: seller, nft_cap, + commission: ask_commission, }; // store the Ask object if (crit_bit::has_key(&book.asks, price)) { @@ -526,7 +656,8 @@ module liquidity_layer::orderbook { owner, id, price: _, - nft_cap + nft_cap, + commission: _, } = remove_ask( &mut book.asks, nft_price, @@ -554,6 +685,7 @@ module liquidity_layer::orderbook { nft_cap, owner: _, price: _, + commission: maybe_commission, } = remove_ask( &mut book.asks, price, @@ -561,9 +693,17 @@ module liquidity_layer::orderbook { ); object::delete(id); - let offer = balance::split(coin::balance_mut(wallet), price); + let bid_offer = balance::split(coin::balance_mut(wallet), price); + + let trade = create_nft_trade_receipt( + &mut bid_offer, + buyer, + &mut maybe_commission, + ctx, + ); + option::destroy_none(maybe_commission); + balance::destroy_zero(bid_offer); - let trade = collection::begin_nft_trade_with(offer, ctx); let nft = safe::trade_nft(nft_cap, trade, safe); transfer(nft, buyer); } @@ -578,9 +718,9 @@ module liquidity_layer::orderbook { nft_cap, paid, buyer, + commission: maybe_commission } = trade; - let amount = balance::value(paid); let nft_cap = option::extract(nft_cap); assert!( @@ -588,8 +728,13 @@ module liquidity_layer::orderbook { err::nft_collection_mismatch(), ); - let trade = - collection::begin_nft_trade_with(balance::split(paid, amount), ctx); + let trade = create_nft_trade_receipt( + paid, + *buyer, + maybe_commission, + ctx, + ); + let nft = safe::trade_nft(nft_cap, trade, safe); transfer(nft, *buyer); } @@ -621,6 +766,64 @@ module liquidity_layer::orderbook { vector::remove(price_level, index) } + fun create_nft_trade_receipt( + paid: &mut Balance, + buyer: address, + maybe_commission: &mut Option, + ctx: &mut TxContext, + ): TradeReceipt { + let amount = balance::value(paid); + + let trade = collection::begin_nft_trade(ctx); + + if (option::is_some(maybe_commission)) { + // the `p`aid amount for the NFT and the commission `c`ut + + let AskCommission { + cut, beneficiary, + } = option::extract(maybe_commission); + + // `p` - `c` goes to seller + collection::pay_for_nft( + &mut trade, + balance::split(paid, amount - cut), + buyer, + ctx, + ); + // `c` goes to the marketplace + collection::pay_for_nft( + &mut trade, + balance::split(paid, cut), + beneficiary, + ctx, + ); + } else { + // no commission, all `p` goes to seller + + collection::pay_for_nft( + &mut trade, + balance::split(paid, amount), + buyer, + ctx, + ); + }; + + trade + } + + fun transfer_bid_commission( + commission: Option>, + ctx: &mut TxContext, + ) { + if (option::is_some(&commission)) { + let BidCommission { beneficiary, cut } = + option::extract(&mut commission); + + transfer(coin::from_balance(cut, ctx), beneficiary); + }; + option::destroy_none(commission); + } + fun no_protection(): WitnessProtectedActions { WitnessProtectedActions { buy_nft: false, diff --git a/sources/safe.move b/sources/safe.move index ba07e6e..1bb242a 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -43,9 +43,9 @@ module liquidity_layer::safe { abort(0) } - public fun trade_nft( + public fun trade_nft( _cap: ExclusiveTransferCap, - trade: TradeReceipt, + trade: TradeReceipt, safe: &mut Safe, ): T { transfer_to_object(trade, safe); From cc74f7b67a79f3a17794fd51d936cfc53efd24c6 Mon Sep 17 00:00:00 2001 From: porkbrain Date: Tue, 11 Oct 2022 16:28:41 +0200 Subject: [PATCH 13/16] Using C for collection generic --- sources/collection.move | 48 ++++---- sources/err.move | 4 - sources/orderbook.move | 237 ++++++++++++++++++++++------------------ sources/safe.move | 2 + 4 files changed, 164 insertions(+), 127 deletions(-) diff --git a/sources/collection.move b/sources/collection.move index 02eb3c6..a5ac10d 100644 --- a/sources/collection.move +++ b/sources/collection.move @@ -5,12 +5,24 @@ module liquidity_layer::collection { use sui::balance::Balance; + use std::vector; use sui::object::{Self, ID, UID}; use sui::transfer::transfer_to_object; use sui::tx_context::TxContext; - use sui::vec_set::{Self, VecSet}; - struct Collection has key, store { + struct Collection has key, store { + id: UID, + } + + /// Only owners of a trade cap are eligible to create `TradeReceipt`s. + /// + /// This enables optional whitelisting of trading contracts by creators. + /// Optional because creators can decide to mint this capability to anyone + /// who "asks" via a permission-less endpoint. + /// + /// `TradeCap` also serves as a bridge between the `Collection` and the + /// witness (outside of the `Collection` type.) + struct TradeCap has key, store { id: UID, } @@ -19,12 +31,11 @@ module liquidity_layer::collection { /// Each trade is done with one of more payments. /// /// IDs of `TradePayment` child objects of this receipt. - payments: VecSet, + payments: vector, } struct TradePayment has key { id: UID, - trade_receipt: ID, amount: Balance, /// The address where the amount should be transferred to. /// This could be either the payment for the seller or a marketplace's @@ -33,26 +44,17 @@ module liquidity_layer::collection { } /// Resolve the trade with [`safe::trade_nft`] - public fun begin_nft_trade_with( - amount: Balance, - beneficiary: address, + public fun begin_nft_trade( + _cap: &mut TradeCap, ctx: &mut TxContext, ): TradeReceipt { - let trade = begin_nft_trade(ctx); - pay_for_nft(&mut trade, amount, beneficiary, ctx); - - trade - } - - /// Resolve the trade with [`safe::trade_nft`] - public fun begin_nft_trade(ctx: &mut TxContext): TradeReceipt { TradeReceipt { id: object::new(ctx), - payments: vec_set::empty(), + payments: vector::empty(), } } - public fun pay_for_nft( + public fun add_nft_payment( trade: &mut TradeReceipt, amount: Balance, beneficiary: address, @@ -60,11 +62,19 @@ module liquidity_layer::collection { ) { let payment = TradePayment { id: object::new(ctx), - trade_receipt: object::id(trade), amount, beneficiary, }; - vec_set::insert(&mut trade.payments, object::id(&payment)); + vector::push_back(&mut trade.payments, object::id(&payment)); transfer_to_object(payment, trade); } + + public fun extract_next_nft_payment( + _witness: W, + _trade: TradeReceipt, + ): TradePayment { + // TODO: wait for feature which enables us to reach child objects via + // id + abort(0) + } } diff --git a/sources/err.move b/sources/err.move index f526d4c..603ac06 100644 --- a/sources/err.move +++ b/sources/err.move @@ -23,8 +23,4 @@ module liquidity_layer::err { public fun action_not_public(): u64 { return Prefix + 04 } - - public fun nft_not_exclusive(): u64 { - return Prefix + 05 - } } diff --git a/sources/orderbook.move b/sources/orderbook.move index 49d9116..791bf68 100644 --- a/sources/orderbook.move +++ b/sources/orderbook.move @@ -4,19 +4,18 @@ module liquidity_layer::orderbook { //! An ask is one NFT with a min price condition. //! //! One can - //! - create a new orderbook between a given collection and a bid token - //! (witness pattern protected); + //! - create a new orderbook between a given collection and a bid token; //! - set publicly accessible actions to be witness protected; //! - open a new bid; //! - cancel an existing bid they own; //! - offer an NFT if collection matches OB collection; //! - cancel an existing NFT offer; - //! - instantly buy an specific NFT; + //! - instantly buy a specific NFT; //! - open bids and asks with a commission on behalf of a user. // TODO: protocol toll - use liquidity_layer::collection::{Self, TradeReceipt}; + use liquidity_layer::collection::{Self, TradeReceipt, TradeCap}; use liquidity_layer::crit_bit::{Self, CB as CBTree}; use liquidity_layer::err; use liquidity_layer::safe::{Self, Safe, ExclusiveTransferCap}; @@ -31,8 +30,11 @@ module liquidity_layer::orderbook { /// A critbit order book implementation. Contains two ordered trees: /// 1. bids ASC /// 2. asks DESC - struct Orderbook has key { + struct Orderbook has key { id: UID, + /// Given off by the collection creator to allow trading of the + /// collection in orderbook. + trade_cap: TradeCap, /// Actions which have a flag set to true can only be called via a /// witness protected implementation. protected_actions: WitnessProtectedActions, @@ -137,7 +139,16 @@ module liquidity_layer::orderbook { /// permissionlessly. struct TradeIntermediate has key { id: UID, + /// in option bcs we want to extract it but cannot destroy shared obj + /// in Sui yet + /// + /// https://github.com/MystenLabs/sui/issues/2083 nft_cap: Option, + /// in option bcs we want to extract it but cannot destroy shared obj + /// in Sui yet + /// + /// https://github.com/MystenLabs/sui/issues/2083 + trade_receipt: Option>, buyer: address, paid: Balance, commission: Option, @@ -150,8 +161,8 @@ module liquidity_layer::orderbook { /// If the `price` is higher than the lowest ask requested price, then we /// execute a trade straight away. Otherwise we add the bid to the /// orderbook's state. - public entry fun create_bid( - book: &mut Orderbook, + public entry fun create_bid( + book: &mut Orderbook, price: u64, wallet: &mut Coin, ctx: &mut TxContext, @@ -159,17 +170,17 @@ module liquidity_layer::orderbook { assert!(book.protected_actions.create_bid, err::action_not_public()); create_bid_(book, price, option::none(), wallet, ctx) } - public fun create_bid_protected( + public fun create_bid_protected( _witness: W, - book: &mut Orderbook, + book: &mut Orderbook, price: u64, wallet: &mut Coin, ctx: &mut TxContext, ) { create_bid_(book, price, option::none(), wallet, ctx) } - public entry fun create_bid_with_commission( - book: &mut Orderbook, + public entry fun create_bid_with_commission( + book: &mut Orderbook, price: u64, beneficiary: address, commission_ft: u64, @@ -183,9 +194,9 @@ module liquidity_layer::orderbook { }; create_bid_(book, price, option::some(commission), wallet, ctx) } - public fun create_bid_with_commission_protected( + public fun create_bid_with_commission_protected( _witness: W, - book: &mut Orderbook, + book: &mut Orderbook, price: u64, beneficiary: address, commission_ft: u64, @@ -201,8 +212,8 @@ module liquidity_layer::orderbook { /// Cancel a bid owned by the sender at given price. If there are two bids /// with the same price, the one created later is cancelled. - public entry fun cancel_bid( - book: &mut Orderbook, + public entry fun cancel_bid( + book: &mut Orderbook, requested_bid_offer_to_cancel: u64, wallet: &mut Coin, ctx: &mut TxContext, @@ -210,9 +221,9 @@ module liquidity_layer::orderbook { assert!(book.protected_actions.cancel_bid, err::action_not_public()); cancel_bid_(book, requested_bid_offer_to_cancel, wallet, ctx) } - public fun cancel_bid_protected( + public fun cancel_bid_protected( _witness: W, - book: &mut Orderbook, + book: &mut Orderbook, requested_bid_offer_to_cancel: u64, wallet: &mut Coin, ctx: &mut TxContext, @@ -224,31 +235,31 @@ module liquidity_layer::orderbook { /// there exists a bid with higher offer than `requsted_tokens`, then trade /// is immeidately executed. Otherwise the NFT is transferred to a newly /// created ask object and the object is inserted to the orderbook. - public entry fun create_ask( - book: &mut Orderbook, + public entry fun create_ask( + book: &mut Orderbook, requsted_tokens: u64, nft_cap: ExclusiveTransferCap, - safe: &mut Safe, + safe: &mut Safe, ctx: &mut TxContext, ) { assert!(book.protected_actions.create_ask, err::action_not_public()); create_ask_(book, requsted_tokens, option::none(), nft_cap, safe, ctx) } - public fun create_ask_protected( + public fun create_ask_protected( _witness: W, - book: &mut Orderbook, + book: &mut Orderbook, requsted_tokens: u64, nft_cap: ExclusiveTransferCap, - safe: &mut Safe, + safe: &mut Safe, ctx: &mut TxContext, ) { create_ask_(book, requsted_tokens, option::none(), nft_cap, safe, ctx) } - public entry fun create_ask_with_commission( - book: &mut Orderbook, + public entry fun create_ask_with_commission( + book: &mut Orderbook, requsted_tokens: u64, nft_cap: ExclusiveTransferCap, - safe: &mut Safe, + safe: &mut Safe, beneficiary: address, commission: u64, ctx: &mut TxContext, @@ -262,12 +273,12 @@ module liquidity_layer::orderbook { book, requsted_tokens, option::some(commission), nft_cap, safe, ctx ) } - public fun create_ask_with_commission_protected( + public fun create_ask_with_commission_protected( _witness: W, - book: &mut Orderbook, + book: &mut Orderbook, requsted_tokens: u64, nft_cap: ExclusiveTransferCap, - safe: &mut Safe, + safe: &mut Safe, beneficiary: address, commission: u64, ctx: &mut TxContext, @@ -286,8 +297,8 @@ module liquidity_layer::orderbook { /// /// This API might be improved in future as we use a different data /// structure for the orderbook. - public entry fun cancel_ask( - book: &mut Orderbook, + public entry fun cancel_ask( + book: &mut Orderbook, nft_price: u64, nft_id: ID, ctx: &mut TxContext, @@ -295,9 +306,9 @@ module liquidity_layer::orderbook { assert!(book.protected_actions.cancel_ask, err::action_not_public()); cancel_ask_(book, nft_price, nft_id, ctx) } - public entry fun cancel_ask_protected( + public entry fun cancel_ask_protected( _witness: W, - book: &mut Orderbook, + book: &mut Orderbook, nft_price: u64, nft_id: ID, ctx: &mut TxContext, @@ -308,29 +319,42 @@ module liquidity_layer::orderbook { /// Buys a specific NFT from the orderbook. This is an atypical OB API as /// with fungible tokens, you just want to get the cheapest ask. /// However, with NFTs, you might want to get a specific one. - public entry fun buy_nft( - book: &mut Orderbook, + public entry fun buy_nft( + book: &mut Orderbook, nft_id: ID, price: u64, wallet: &mut Coin, - safe: &mut Safe, + safe: &mut Safe, ctx: &mut TxContext, ) { assert!(book.protected_actions.buy_nft, err::action_not_public()); buy_nft_(book, nft_id, price, wallet, safe, ctx) } - public entry fun buy_nft_protected( + public entry fun buy_nft_protected( _witness: W, - book: &mut Orderbook, + book: &mut Orderbook, nft_id: ID, price: u64, wallet: &mut Coin, - safe: &mut Safe, + safe: &mut Safe, ctx: &mut TxContext, ) { buy_nft_(book, nft_id, price, wallet, safe, ctx) } + /// When a bid is created and there's an ask with a lower price, then the + /// trade cannot be resolved immiedately. + /// That's because we don't know the `Safe` ID up front in OB. + /// Therefore, the tx creates `TradeIntermediate` which then has to be + /// permission-lessly resolved via this endpoint. + public fun finish_trade( + trade: &mut TradeIntermediate, + safe: &mut Safe, + ctx: &mut TxContext, + ) { + finish_trade_(trade, safe, ctx) + } + /// `C`ollection kind of NFTs to be traded, and `F`ungible `T`oken to be /// quoted for an NFT in such a collection. /// @@ -340,64 +364,59 @@ module liquidity_layer::orderbook { /// To implement specific logic in your smart contract, you can toggle the /// protection on specific actions. That will make them only accessible via /// witness protected methods. - public fun create( - _witness: W, + public entry fun create( + trade_cap: TradeCap, ctx: &mut TxContext, - ): Orderbook { - create_(no_protection(), ctx) + ) { + let ob = create_(no_protection(), trade_cap, ctx); + share_object(ob); } - - /// When a bid is created and there's an ask with a lower price, then the - /// trade cannot be resolved immiedately. - /// That's because we don't know the `Safe` ID up front in OB. - /// Therefore, the tx creates `TradeIntermediate` which then has to be - /// permission-lessly resolved via this endpoint. - public fun finish_trade( - trade: &mut TradeIntermediate, - safe: &mut Safe, + public fun create_protected( + _witness: W, + trade_cap: TradeCap, ctx: &mut TxContext, - ) { - finish_trade_(trade, safe, ctx) + ): Orderbook { + create_(no_protection(), trade_cap, ctx) } - public fun toggle_protection_on_buy_nft( + public fun toggle_protection_on_buy_nft( _witness: W, - book: &mut Orderbook, + book: &mut Orderbook, ) { book.protected_actions.buy_nft = !book.protected_actions.buy_nft; } - public fun toggle_protection_on_cancel_ask( + public fun toggle_protection_on_cancel_ask( _witness: W, - book: &mut Orderbook, + book: &mut Orderbook, ) { book.protected_actions.cancel_ask = !book.protected_actions.cancel_ask; } - public fun toggle_protection_on_cancel_bid( + public fun toggle_protection_on_cancel_bid( _witness: W, - book: &mut Orderbook, + book: &mut Orderbook, ) { book.protected_actions.cancel_bid = !book.protected_actions.cancel_bid; } - public fun toggle_protection_on_create_ask( + public fun toggle_protection_on_create_ask( _witness: W, - book: &mut Orderbook, + book: &mut Orderbook, ) { book.protected_actions.create_ask = !book.protected_actions.create_ask; } - public fun toggle_protection_on_create_bid( + public fun toggle_protection_on_create_bid( _witness: W, - book: &mut Orderbook, + book: &mut Orderbook, ) { book.protected_actions.create_bid = !book.protected_actions.create_bid; } - public fun borrow_bids( - book: &Orderbook, + public fun borrow_bids( + book: &Orderbook, ): &CBTree>> { &book.bids } @@ -410,8 +429,8 @@ module liquidity_layer::orderbook { bid.owner } - public fun borrow_asks( - book: &Orderbook, + public fun borrow_asks( + book: &Orderbook, ): &CBTree> { &book.asks } @@ -428,22 +447,24 @@ module liquidity_layer::orderbook { ask.owner } - fun create_( + fun create_( protected_actions: WitnessProtectedActions, + trade_cap: TradeCap, ctx: &mut TxContext, - ): Orderbook { + ): Orderbook { let id = object::new(ctx); - Orderbook { + Orderbook { id, + trade_cap, protected_actions, asks: crit_bit::empty(), bids: crit_bit::empty(), } } - fun create_bid_( - book: &mut Orderbook, + fun create_bid_( + book: &mut Orderbook, price: u64, bid_commission: Option>, wallet: &mut Coin, @@ -490,6 +511,10 @@ module liquidity_layer::orderbook { nft_cap: option::some(nft_cap), commission: ask_commission, buyer, + trade_receipt: option::some(collection::begin_nft_trade( + &mut book.trade_cap, + ctx, + )), paid: bid_offer, }); @@ -516,8 +541,8 @@ module liquidity_layer::orderbook { } } - fun cancel_bid_( - book: &mut Orderbook, + fun cancel_bid_( + book: &mut Orderbook, requested_bid_offer_to_cancel: u64, wallet: &mut Coin, ctx: &mut TxContext, @@ -566,12 +591,12 @@ module liquidity_layer::orderbook { option::destroy_none(commission); } - fun create_ask_( - book: &mut Orderbook, + fun create_ask_( + book: &mut Orderbook, price: u64, ask_commission: Option, nft_cap: ExclusiveTransferCap, - safe: &mut Safe, + safe: &mut Safe, ctx: &mut TxContext, ) { assert!( @@ -606,7 +631,9 @@ module liquidity_layer::orderbook { commission: bid_commission, } = bid; - let trade = create_nft_trade_receipt( + let trade = collection::begin_nft_trade(&mut book.trade_cap, ctx); + pay_for_nft( + &mut trade, &mut bid_offer, buyer, &mut ask_commission, @@ -615,7 +642,7 @@ module liquidity_layer::orderbook { option::destroy_none(ask_commission); balance::destroy_zero(bid_offer); - let nft = safe::trade_nft(nft_cap, trade, safe); + let nft = safe::trade_nft(nft_cap, trade, safe); transfer(nft, buyer); transfer_bid_commission(bid_commission, ctx); @@ -644,8 +671,8 @@ module liquidity_layer::orderbook { } } - fun cancel_ask_( - book: &mut Orderbook, + fun cancel_ask_( + book: &mut Orderbook, nft_price: u64, nft_id: ID, ctx: &mut TxContext, @@ -670,12 +697,12 @@ module liquidity_layer::orderbook { transfer(nft_cap, sender); } - fun buy_nft_( - book: &mut Orderbook, + fun buy_nft_( + book: &mut Orderbook, nft_id: ID, price: u64, wallet: &mut Coin, - safe: &mut Safe, + safe: &mut Safe, ctx: &mut TxContext, ) { let buyer = tx_context::sender(ctx); @@ -695,7 +722,9 @@ module liquidity_layer::orderbook { let bid_offer = balance::split(coin::balance_mut(wallet), price); - let trade = create_nft_trade_receipt( + let trade = collection::begin_nft_trade(&mut book.trade_cap, ctx); + pay_for_nft( + &mut trade, &mut bid_offer, buyer, &mut maybe_commission, @@ -704,23 +733,25 @@ module liquidity_layer::orderbook { option::destroy_none(maybe_commission); balance::destroy_zero(bid_offer); - let nft = safe::trade_nft(nft_cap, trade, safe); + let nft = safe::trade_nft(nft_cap, trade, safe); transfer(nft, buyer); } - fun finish_trade_( + fun finish_trade_( trade: &mut TradeIntermediate, - safe: &mut Safe, + safe: &mut Safe, ctx: &mut TxContext, ) { let TradeIntermediate { id: _, nft_cap, + trade_receipt, paid, buyer, - commission: maybe_commission + commission: maybe_commission, } = trade; + let trade = option::extract(trade_receipt); let nft_cap = option::extract(nft_cap); assert!( @@ -728,14 +759,15 @@ module liquidity_layer::orderbook { err::nft_collection_mismatch(), ); - let trade = create_nft_trade_receipt( + pay_for_nft( + &mut trade, paid, *buyer, maybe_commission, ctx, ); - let nft = safe::trade_nft(nft_cap, trade, safe); + let nft = safe::trade_nft(nft_cap, trade, safe); transfer(nft, *buyer); } @@ -766,16 +798,15 @@ module liquidity_layer::orderbook { vector::remove(price_level, index) } - fun create_nft_trade_receipt( + fun pay_for_nft( + trade: &mut TradeReceipt, paid: &mut Balance, buyer: address, maybe_commission: &mut Option, ctx: &mut TxContext, - ): TradeReceipt { + ) { let amount = balance::value(paid); - let trade = collection::begin_nft_trade(ctx); - if (option::is_some(maybe_commission)) { // the `p`aid amount for the NFT and the commission `c`ut @@ -784,15 +815,15 @@ module liquidity_layer::orderbook { } = option::extract(maybe_commission); // `p` - `c` goes to seller - collection::pay_for_nft( - &mut trade, + collection::add_nft_payment( + trade, balance::split(paid, amount - cut), buyer, ctx, ); // `c` goes to the marketplace - collection::pay_for_nft( - &mut trade, + collection::add_nft_payment( + trade, balance::split(paid, cut), beneficiary, ctx, @@ -800,15 +831,13 @@ module liquidity_layer::orderbook { } else { // no commission, all `p` goes to seller - collection::pay_for_nft( - &mut trade, + collection::add_nft_payment( + trade, balance::split(paid, amount), buyer, ctx, ); }; - - trade } fun transfer_bid_commission( diff --git a/sources/safe.move b/sources/safe.move index 1bb242a..b240c55 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -1,5 +1,7 @@ //! Taken from https://github.com/MystenLabs/sui/pull/4887/files#diff-96b0dc07cabd79292618c993cd473c43ca81cd2f742266014967cdea1a7c6186 //! and modified. +//! +//! This will eventually be part of a different package. module liquidity_layer::safe { use sui::object::{ID, UID}; From 3a6cb384909c301c854c1b18daad87de24d17155 Mon Sep 17 00:00:00 2001 From: porkbrain Date: Tue, 11 Oct 2022 17:05:46 +0200 Subject: [PATCH 14/16] Some more docs and TBDs about TradeCao --- sources/collection.move | 8 ++++++++ sources/safe.move | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/sources/collection.move b/sources/collection.move index a5ac10d..cb57e27 100644 --- a/sources/collection.move +++ b/sources/collection.move @@ -22,6 +22,10 @@ module liquidity_layer::collection { /// /// `TradeCap` also serves as a bridge between the `Collection` and the /// witness (outside of the `Collection` type.) + /// + /// TBD: Should we implement recovability? It would compilate the design + /// because we would have to work with a shared object that would have to + /// be included in every call. struct TradeCap has key, store { id: UID, } @@ -77,4 +81,8 @@ module liquidity_layer::collection { // id abort(0) } + + public fun has_some_nft_payment(trade: &TradeReceipt): bool { + !vector::is_empty(&trade.payments) + } } diff --git a/sources/safe.move b/sources/safe.move index b240c55..2446ebb 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -6,7 +6,7 @@ module liquidity_layer::safe { use sui::object::{ID, UID}; use sui::transfer::transfer_to_object; - use liquidity_layer::collection::{TradeReceipt}; + use liquidity_layer::collection::{Self, TradeReceipt}; /// A shared object for storing NFT's of type `T`, owned by the holder of a unique `OwnerCap`. /// Permissions to allow others to list NFT's can be granted via TransferCap's and BorrowCap's @@ -50,6 +50,12 @@ module liquidity_layer::safe { trade: TradeReceipt, safe: &mut Safe, ): T { + // we cannot know whether the trade payment was honest or whether there + // was a side payment, but at least we know that the payment was + // considered and therefore if a contract wanted to avoid royalties, + // they'd have to be _explicitly_ malicious + assert!(collection::has_some_nft_payment(&trade), 0); + transfer_to_object(trade, safe); abort(0) From 3320b3148f81bec0c98e811c8730066567556850 Mon Sep 17 00:00:00 2001 From: porkbrain Date: Tue, 11 Oct 2022 17:07:09 +0200 Subject: [PATCH 15/16] Finish trade is now an entry point --- sources/orderbook.move | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/orderbook.move b/sources/orderbook.move index 791bf68..a45e90f 100644 --- a/sources/orderbook.move +++ b/sources/orderbook.move @@ -347,7 +347,7 @@ module liquidity_layer::orderbook { /// That's because we don't know the `Safe` ID up front in OB. /// Therefore, the tx creates `TradeIntermediate` which then has to be /// permission-lessly resolved via this endpoint. - public fun finish_trade( + public entry fun finish_trade( trade: &mut TradeIntermediate, safe: &mut Safe, ctx: &mut TxContext, From a3822e07fb0d55b9bd6f8b8feda97798f5a94760 Mon Sep 17 00:00:00 2001 From: porkbrain Date: Tue, 18 Oct 2022 01:20:42 +0200 Subject: [PATCH 16/16] Implementing whitelist --- sources/bidding.move | 147 ++++++++++++++++++++++++++++++++++++++++ sources/collection.move | 53 ++++++++++----- sources/orderbook.move | 77 +++++++++++++-------- sources/safe.move | 32 +++++++-- 4 files changed, 258 insertions(+), 51 deletions(-) create mode 100644 sources/bidding.move diff --git a/sources/bidding.move b/sources/bidding.move new file mode 100644 index 0000000..dadcdb4 --- /dev/null +++ b/sources/bidding.move @@ -0,0 +1,147 @@ +module liquidity_layer::bidding { + use liquidity_layer::collection::{Self, TradeReceipt, TradingWhitelist}; + use liquidity_layer::err; + use liquidity_layer::safe::{Self, Safe, TransferCap}; + use std::option::{Self, Option}; + use sui::balance::{Self, Balance}; + use sui::coin::{Self, Coin}; + use sui::object::{Self, ID, UID}; + use sui::transfer::{transfer, share_object}; + use sui::tx_context::{Self, TxContext}; + + struct Bidding has key { + id: UID, + } + + struct Bid has key { + id: UID, + nft: ID, + buyer: address, + offer: Balance, + commission: Option>, + } + + /// Enables collection of wallet/marketplace collection for buying NFTs. + /// 1. user bids via wallet to buy NFT for `p`, wallet wants fee `f` + /// 2. when executed, `p` goes to seller and `f` goes to wallet. + /// + /// + /// TODO: deduplicate with OB + struct BidCommission has store { + /// This is given to the facilitator of the trade. + cut: Balance, + /// A new `Coin` object is created and sent to this address. + beneficiary: address, + } + + /// TODO: deduplicate with OB + struct AskCommission has store, drop { + /// How many tokens of the transferred amount should go to the party + /// which holds the private key of `beneficiary` address. + /// + /// Always less than ask price. + cut: u64, + /// A new `Coin` object is created and sent to this address. + beneficiary: address, + } + + public entry fun create_bid( + _nft: address, + _amount: u64, + _wallet: &mut Coin, + _ctx: &mut TxContext, + ) { + abort(0) + } + + // TODO: opt commission + public entry fun sell_nft( + contract: &Bidding, + bid: &mut Bid, + nft_cap: TransferCap, + safe: &mut Safe, + whitelist: &TradingWhitelist, + ctx: &mut TxContext, + ) { + assert!( + object::id(safe) == safe::transfer_cap_safe_id(&nft_cap), + err::nft_collection_mismatch(), + ); + assert!( + bid.nft == safe::transfer_cap_nft_id(&nft_cap), + err::nft_collection_mismatch(), + ); + + let trade = collection::begin_nft_trade(&contract.id, ctx); + let ask_commission = option::none(); + pay_for_nft( + &mut trade, + &mut bid.offer, + bid.buyer, + &mut ask_commission, + ctx, + ); + option::destroy_none(ask_commission); + + let nft = safe::trade_nft(nft_cap, trade, whitelist, safe); + transfer(nft, bid.buyer); + + transfer_bid_commission(&mut bid.commission, ctx); + } + + // TODO: dedup + fun pay_for_nft( + trade: &mut TradeReceipt, + paid: &mut Balance, + buyer: address, + maybe_commission: &mut Option, + ctx: &mut TxContext, + ) { + let amount = balance::value(paid); + + if (option::is_some(maybe_commission)) { + // the `p`aid amount for the NFT and the commission `c`ut + + let AskCommission { + cut, beneficiary, + } = option::extract(maybe_commission); + + // `p` - `c` goes to seller + collection::add_nft_payment( + trade, + balance::split(paid, amount - cut), + buyer, + ctx, + ); + // `c` goes to the marketplace + collection::add_nft_payment( + trade, + balance::split(paid, cut), + beneficiary, + ctx, + ); + } else { + // no commission, all `p` goes to seller + + collection::add_nft_payment( + trade, + balance::split(paid, amount), + buyer, + ctx, + ); + }; + } + + // TODO: dedup + fun transfer_bid_commission( + commission: &mut Option>, + ctx: &mut TxContext, + ) { + if (option::is_some(commission)) { + let BidCommission { beneficiary, cut } = + option::extract(commission); + + transfer(coin::from_balance(cut, ctx), beneficiary); + }; + } +} diff --git a/sources/collection.move b/sources/collection.move index cb57e27..5bfaeb0 100644 --- a/sources/collection.move +++ b/sources/collection.move @@ -5,33 +5,32 @@ module liquidity_layer::collection { use sui::balance::Balance; + use sui::vec_set::{Self, VecSet}; use std::vector; use sui::object::{Self, ID, UID}; use sui::transfer::transfer_to_object; use sui::tx_context::TxContext; + use std::option::{Self, Option}; struct Collection has key, store { id: UID, } - /// Only owners of a trade cap are eligible to create `TradeReceipt`s. - /// - /// This enables optional whitelisting of trading contracts by creators. - /// Optional because creators can decide to mint this capability to anyone - /// who "asks" via a permission-less endpoint. - /// - /// `TradeCap` also serves as a bridge between the `Collection` and the - /// witness (outside of the `Collection` type.) - /// - /// TBD: Should we implement recovability? It would compilate the design - /// because we would have to work with a shared object that would have to - /// be included in every call. - struct TradeCap has key, store { + struct TradingWhitelist has key { id: UID, + authority: address, + /// If None, then there's no whitelist and everyone is allowed. + /// Otherwise the ID must be in the vec set. + /// + /// Then we assert that the source from `TradeReceipt` is included in + /// this set. + entities: Option>, } struct TradeReceipt has key, store { id: UID, + /// ID of the source entity which began the trade. + source: ID, /// Each trade is done with one of more payments. /// /// IDs of `TradePayment` child objects of this receipt. @@ -48,12 +47,19 @@ module liquidity_layer::collection { } /// Resolve the trade with [`safe::trade_nft`] - public fun begin_nft_trade( - _cap: &mut TradeCap, + /// + /// Settlement + /// + /// The assumption here is that getting a reference to a UID can be only + /// done from within the contract that created an entity. Therefore, this + /// is kind of similar to a witness pattern but works with UID instead. + public fun begin_nft_trade( + source: &UID, ctx: &mut TxContext, ): TradeReceipt { TradeReceipt { id: object::new(ctx), + source: object::uid_to_inner(source), payments: vector::empty(), } } @@ -77,12 +83,25 @@ module liquidity_layer::collection { _witness: W, _trade: TradeReceipt, ): TradePayment { - // TODO: wait for feature which enables us to reach child objects via - // id + // TODO: wait for feature which enables us to reach child objects + // dynamically https://github.com/MystenLabs/sui/issues/4203 abort(0) } public fun has_some_nft_payment(trade: &TradeReceipt): bool { !vector::is_empty(&trade.payments) } + + public fun is_trade_source_whitelisted( + trade: &TradeReceipt, + whitelist: &TradingWhitelist, + ): bool { + if (option::is_none(&whitelist.entities)) { + return true + }; + + let entities = option::borrow(&whitelist.entities); + + vec_set::contains(entities, &trade.source) + } } diff --git a/sources/orderbook.move b/sources/orderbook.move index a45e90f..35c10d6 100644 --- a/sources/orderbook.move +++ b/sources/orderbook.move @@ -14,8 +14,9 @@ module liquidity_layer::orderbook { //! - open bids and asks with a commission on behalf of a user. // TODO: protocol toll + // TODO: eviction of lowest bid/highest ask on OOM - use liquidity_layer::collection::{Self, TradeReceipt, TradeCap}; + use liquidity_layer::collection::{Self, TradeReceipt, TradingWhitelist}; use liquidity_layer::crit_bit::{Self, CB as CBTree}; use liquidity_layer::err; use liquidity_layer::safe::{Self, Safe, ExclusiveTransferCap}; @@ -32,9 +33,6 @@ module liquidity_layer::orderbook { /// 2. asks DESC struct Orderbook has key { id: UID, - /// Given off by the collection creator to allow trading of the - /// collection in orderbook. - trade_cap: TradeCap, /// Actions which have a flag set to true can only be called via a /// witness protected implementation. protected_actions: WitnessProtectedActions, @@ -84,7 +82,7 @@ module liquidity_layer::orderbook { owner: address, /// If the NFT is offered via a marketplace or a wallet, the /// faciliatator can optionally set how many tokens they want to claim - /// on top of the + /// on top of the offer. commission: Option>, } /// Enables collection of wallet/marketplace collection for buying NFTs. @@ -240,10 +238,13 @@ module liquidity_layer::orderbook { requsted_tokens: u64, nft_cap: ExclusiveTransferCap, safe: &mut Safe, + whitelist: &TradingWhitelist, ctx: &mut TxContext, ) { assert!(book.protected_actions.create_ask, err::action_not_public()); - create_ask_(book, requsted_tokens, option::none(), nft_cap, safe, ctx) + create_ask_( + book, requsted_tokens, option::none(), nft_cap, safe, whitelist, ctx + ) } public fun create_ask_protected( _witness: W, @@ -251,17 +252,21 @@ module liquidity_layer::orderbook { requsted_tokens: u64, nft_cap: ExclusiveTransferCap, safe: &mut Safe, + whitelist: &TradingWhitelist, ctx: &mut TxContext, ) { - create_ask_(book, requsted_tokens, option::none(), nft_cap, safe, ctx) + create_ask_( + book, requsted_tokens, option::none(), nft_cap, safe, whitelist, ctx + ) } public entry fun create_ask_with_commission( book: &mut Orderbook, requsted_tokens: u64, nft_cap: ExclusiveTransferCap, - safe: &mut Safe, beneficiary: address, commission: u64, + safe: &mut Safe, + whitelist: &TradingWhitelist, ctx: &mut TxContext, ) { assert!(book.protected_actions.create_ask, err::action_not_public()); @@ -270,7 +275,13 @@ module liquidity_layer::orderbook { beneficiary, }; create_ask_( - book, requsted_tokens, option::some(commission), nft_cap, safe, ctx + book, + requsted_tokens, + option::some(commission), + nft_cap, + safe, + whitelist, + ctx, ) } public fun create_ask_with_commission_protected( @@ -278,9 +289,10 @@ module liquidity_layer::orderbook { book: &mut Orderbook, requsted_tokens: u64, nft_cap: ExclusiveTransferCap, - safe: &mut Safe, beneficiary: address, commission: u64, + safe: &mut Safe, + whitelist: &TradingWhitelist, ctx: &mut TxContext, ) { let commission = AskCommission { @@ -288,7 +300,13 @@ module liquidity_layer::orderbook { beneficiary, }; create_ask_( - book, requsted_tokens, option::some(commission), nft_cap, safe, ctx + book, + requsted_tokens, + option::some(commission), + nft_cap, + safe, + whitelist, + ctx, ) } @@ -325,10 +343,11 @@ module liquidity_layer::orderbook { price: u64, wallet: &mut Coin, safe: &mut Safe, + whitelist: &TradingWhitelist, ctx: &mut TxContext, ) { assert!(book.protected_actions.buy_nft, err::action_not_public()); - buy_nft_(book, nft_id, price, wallet, safe, ctx) + buy_nft_(book, nft_id, price, wallet, safe, whitelist, ctx) } public entry fun buy_nft_protected( _witness: W, @@ -337,9 +356,10 @@ module liquidity_layer::orderbook { price: u64, wallet: &mut Coin, safe: &mut Safe, + whitelist: &TradingWhitelist, ctx: &mut TxContext, ) { - buy_nft_(book, nft_id, price, wallet, safe, ctx) + buy_nft_(book, nft_id, price, wallet, safe, whitelist, ctx) } /// When a bid is created and there's an ask with a lower price, then the @@ -350,9 +370,10 @@ module liquidity_layer::orderbook { public entry fun finish_trade( trade: &mut TradeIntermediate, safe: &mut Safe, + whitelist: &TradingWhitelist, ctx: &mut TxContext, ) { - finish_trade_(trade, safe, ctx) + finish_trade_(trade, safe, whitelist, ctx) } /// `C`ollection kind of NFTs to be traded, and `F`ungible `T`oken to be @@ -365,18 +386,16 @@ module liquidity_layer::orderbook { /// protection on specific actions. That will make them only accessible via /// witness protected methods. public entry fun create( - trade_cap: TradeCap, ctx: &mut TxContext, ) { - let ob = create_(no_protection(), trade_cap, ctx); + let ob = create_(no_protection(), ctx); share_object(ob); } public fun create_protected( _witness: W, - trade_cap: TradeCap, ctx: &mut TxContext, ): Orderbook { - create_(no_protection(), trade_cap, ctx) + create_(no_protection(), ctx) } public fun toggle_protection_on_buy_nft( @@ -449,14 +468,12 @@ module liquidity_layer::orderbook { fun create_( protected_actions: WitnessProtectedActions, - trade_cap: TradeCap, ctx: &mut TxContext, ): Orderbook { let id = object::new(ctx); Orderbook { id, - trade_cap, protected_actions, asks: crit_bit::empty(), bids: crit_bit::empty(), @@ -511,10 +528,9 @@ module liquidity_layer::orderbook { nft_cap: option::some(nft_cap), commission: ask_commission, buyer, - trade_receipt: option::some(collection::begin_nft_trade( - &mut book.trade_cap, - ctx, - )), + trade_receipt: option::some( + collection::begin_nft_trade(&book.id, ctx), + ), paid: bid_offer, }); @@ -597,6 +613,7 @@ module liquidity_layer::orderbook { ask_commission: Option, nft_cap: ExclusiveTransferCap, safe: &mut Safe, + whitelist: &TradingWhitelist, ctx: &mut TxContext, ) { assert!( @@ -631,7 +648,7 @@ module liquidity_layer::orderbook { commission: bid_commission, } = bid; - let trade = collection::begin_nft_trade(&mut book.trade_cap, ctx); + let trade = collection::begin_nft_trade(&book.id, ctx); pay_for_nft( &mut trade, &mut bid_offer, @@ -642,7 +659,7 @@ module liquidity_layer::orderbook { option::destroy_none(ask_commission); balance::destroy_zero(bid_offer); - let nft = safe::trade_nft(nft_cap, trade, safe); + let nft = safe::trade_nft_exclusive(nft_cap, trade, whitelist, safe); transfer(nft, buyer); transfer_bid_commission(bid_commission, ctx); @@ -703,6 +720,7 @@ module liquidity_layer::orderbook { price: u64, wallet: &mut Coin, safe: &mut Safe, + whitelist: &TradingWhitelist, ctx: &mut TxContext, ) { let buyer = tx_context::sender(ctx); @@ -722,7 +740,7 @@ module liquidity_layer::orderbook { let bid_offer = balance::split(coin::balance_mut(wallet), price); - let trade = collection::begin_nft_trade(&mut book.trade_cap, ctx); + let trade = collection::begin_nft_trade(&book.id, ctx); pay_for_nft( &mut trade, &mut bid_offer, @@ -733,13 +751,14 @@ module liquidity_layer::orderbook { option::destroy_none(maybe_commission); balance::destroy_zero(bid_offer); - let nft = safe::trade_nft(nft_cap, trade, safe); + let nft = safe::trade_nft_exclusive(nft_cap, trade, whitelist, safe); transfer(nft, buyer); } fun finish_trade_( trade: &mut TradeIntermediate, safe: &mut Safe, + whitelist: &TradingWhitelist, ctx: &mut TxContext, ) { let TradeIntermediate { @@ -767,7 +786,7 @@ module liquidity_layer::orderbook { ctx, ); - let nft = safe::trade_nft(nft_cap, trade, safe); + let nft = safe::trade_nft_exclusive(nft_cap, trade, whitelist, safe); transfer(nft, *buyer); } diff --git a/sources/safe.move b/sources/safe.move index 2446ebb..87b589f 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -6,11 +6,11 @@ module liquidity_layer::safe { use sui::object::{ID, UID}; use sui::transfer::transfer_to_object; - use liquidity_layer::collection::{Self, TradeReceipt}; + use liquidity_layer::collection::{Self, TradeReceipt, TradingWhitelist}; /// A shared object for storing NFT's of type `T`, owned by the holder of a unique `OwnerCap`. /// Permissions to allow others to list NFT's can be granted via TransferCap's and BorrowCap's - struct Safe has key { + struct Safe has key { id: UID, owner: address, // ... contains the fields from MR @@ -33,6 +33,8 @@ module liquidity_layer::safe { nft_id: ID, } + // TODO: should this be a separate type? it's more explicit, but need to + // duplicate fns. we could also have it as a flag on `TransferCap` struct ExclusiveTransferCap has key, store { id: UID, safe_id: ID, @@ -45,17 +47,29 @@ module liquidity_layer::safe { abort(0) } - public fun trade_nft( + public fun trade_nft( + _cap: TransferCap, + _trade: TradeReceipt, + _whitelist: &TradingWhitelist, + _safe: &mut Safe, + ): C { + abort(0) + } + + public fun trade_nft_exclusive( _cap: ExclusiveTransferCap, trade: TradeReceipt, - safe: &mut Safe, - ): T { + whitelist: &TradingWhitelist, + safe: &mut Safe, + ): C { // we cannot know whether the trade payment was honest or whether there // was a side payment, but at least we know that the payment was // considered and therefore if a contract wanted to avoid royalties, // they'd have to be _explicitly_ malicious assert!(collection::has_some_nft_payment(&trade), 0); + assert!(collection::is_trade_source_whitelisted(&trade, whitelist), 0); + transfer_to_object(trade, safe); abort(0) @@ -72,4 +86,12 @@ module liquidity_layer::safe { public fun exclusive_transfer_cap_nft_id(cap: &ExclusiveTransferCap): ID { cap.nft_id } + + public fun transfer_cap_safe_id(cap: &TransferCap): ID { + cap.safe_id + } + + public fun transfer_cap_nft_id(cap: &TransferCap): ID { + cap.nft_id + } }