From d336563e36434b80824bc0ae394da242b3e70e20 Mon Sep 17 00:00:00 2001 From: Nuno Boavida <45330362+nmboavida@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:18:55 +0000 Subject: [PATCH 1/4] Launchpad fee cuts for promoter addresses --- .../sources/launchpad/fees/fee_cut.move | 114 ++++++++++++++++++ .../sources/launchpad/fees/flat_fee.move | 7 ++ .../launchpad/sources/launchpad/listing.move | 32 +++++ .../launchpad/sources/launchpad/proceeds.move | 3 +- 4 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 contracts/launchpad/sources/launchpad/fees/fee_cut.move diff --git a/contracts/launchpad/sources/launchpad/fees/fee_cut.move b/contracts/launchpad/sources/launchpad/fees/fee_cut.move new file mode 100644 index 00000000..53b6cebd --- /dev/null +++ b/contracts/launchpad/sources/launchpad/fees/fee_cut.move @@ -0,0 +1,114 @@ +module ob_launchpad::fee_cut { + use sui::tx_context::TxContext; + use sui::table::{Self, Table}; + use sui::table_vec::{Self, TableVec}; + use sui::balance; + use sui::coin; + use sui::transfer; + + use ob_launchpad::listing::{Self, Listing}; + use ob_launchpad::marketplace::Marketplace; + use ob_launchpad::proceeds; + + friend ob_launchpad::flat_fee; + + const EInvalidAddress: u64 = 0; + + struct FeeCutList has store { + fee_cut: TableVec, + address_map: Table, + } + + struct Cut has store, drop { + addr: address, + cut: u64 + } + + public fun add_fee_cut( + marketplace: &Marketplace, + listing: &mut Listing, + addr: address, + cut_amount: u64, + ctx: &mut TxContext, + ) { + listing::assert_listing_marketplace_match(marketplace, listing); + listing::assert_correct_admin_or_member(marketplace, listing, ctx); + + let fee_cut_exists = listing::fee_cut_exists(listing); + + if (!fee_cut_exists) { + listing::add_fee_cut_internal(listing, FeeCutList { + fee_cut: table_vec::empty(ctx), + address_map: table::new(ctx), + }); + }; + + let fee_cut = listing::borrow_fee_cut_mut(listing); + + if (!table::contains(&fee_cut.address_map, addr)) { + let addr_idx = table_vec::length(&fee_cut.fee_cut); + + table::add(&mut fee_cut.address_map, addr, addr_idx); + + let cut = Cut { addr, cut: cut_amount }; + + table_vec::push_back(&mut fee_cut.fee_cut, cut); + } else { + let addr_idx = *table::borrow(&fee_cut.address_map, addr); + let fee_cut = table_vec::borrow_mut(&mut fee_cut.fee_cut, addr_idx); + + assert!(fee_cut.addr == addr, EInvalidAddress); + + fee_cut.cut = fee_cut.cut + cut_amount; + }; + } + + public(friend) fun collect_fee_cuts_if_any( + marketplace: &Marketplace, + listing: &mut Listing, + ctx: &mut TxContext, + ) { + listing::assert_listing_marketplace_match(marketplace, listing); + listing::assert_correct_admin_or_member(marketplace, listing, ctx); + + let fee_cut_exists = listing::fee_cut_exists(listing); + + // Pay fee cuts + if (fee_cut_exists) { + let to_transfer = table_vec::empty(ctx); + + let fee_cut = listing::borrow_fee_cut_mut(listing); + + let len = table_vec::length(&fee_cut.fee_cut); + while (len > 0) { + table_vec::push_back(&mut to_transfer, table_vec::pop_back(&mut fee_cut.fee_cut)); + + len = len - 1; + }; + + let proceeds = listing::borrow_proceeds_mut(listing); + let balance = proceeds::balance_mut(proceeds); + + let len = table_vec::length(&to_transfer); + while (len > 0) { + let cut_data = table_vec::pop_back(&mut to_transfer); + + let cut_balance = balance::split( + balance, + cut_data.cut, + ); + + let cut = coin::from_balance(cut_balance, ctx); + + transfer::public_transfer( + cut, + cut_data.addr, + ); + + len = len - 1; + }; + + table_vec::destroy_empty(to_transfer); + }; + } +} diff --git a/contracts/launchpad/sources/launchpad/fees/flat_fee.move b/contracts/launchpad/sources/launchpad/fees/flat_fee.move index ea53dd88..cd62a723 100644 --- a/contracts/launchpad/sources/launchpad/fees/flat_fee.move +++ b/contracts/launchpad/sources/launchpad/fees/flat_fee.move @@ -14,6 +14,7 @@ module ob_launchpad::flat_fee { use ob_launchpad::proceeds; use ob_launchpad::listing::{Self, Listing}; use ob_launchpad::marketplace::{Self as mkt, Marketplace}; + use ob_launchpad::fee_cut; /// `Listing` did not have `FlatFee` policy const EInvalidFeePolicy: u64 = 1; @@ -55,6 +56,12 @@ module ob_launchpad::flat_fee { listing::assert_listing_marketplace_match(marketplace, listing); listing::assert_correct_admin_or_member(marketplace, listing, ctx); + fee_cut::collect_fee_cuts_if_any( + marketplace, + listing, + ctx, + ); + let (proceeds_value, listing_receiver) = { let proceeds = listing::borrow_proceeds(listing); let listing_receiver = listing::receiver(listing); diff --git a/contracts/launchpad/sources/launchpad/listing.move b/contracts/launchpad/sources/launchpad/listing.move index 4a5738c6..def3070d 100644 --- a/contracts/launchpad/sources/launchpad/listing.move +++ b/contracts/launchpad/sources/launchpad/listing.move @@ -54,6 +54,7 @@ module ob_launchpad::listing { use ob_kiosk::ob_kiosk; friend ob_launchpad::flat_fee; + friend ob_launchpad::fee_cut; friend ob_launchpad::market_whitelist; friend ob_launchpad::dutch_auction; friend ob_launchpad::fixed_price; @@ -129,6 +130,7 @@ module ob_launchpad::listing { struct MembersDfKey has store, copy, drop {} struct WhitelistDfKey has store, copy, drop { venue_id: ID } struct StartSaleDfKey has store, copy, drop { venue_id: ID } + struct FeeCutDfKey has store, copy, drop { } // === Events === @@ -1203,6 +1205,36 @@ module ob_launchpad::listing { df::borrow_mut(&mut listing.id, WhitelistDfKey { venue_id }) } + /// Adds `FeeCut` object. We use a generic `FeeCut` to represent the + /// `FeeCut` type object to avoid dependency cycles. + /// + /// Warning: This function is not safe to be shared, it does not check + /// for permissions and should ONLY be used by the function + /// `fee_cut::add_fee_cut` + public(friend) fun add_fee_cut_internal( + listing: &mut Listing, + fee_cut: FeeCut + ) { + df::add(&mut listing.id, FeeCutDfKey { }, fee_cut); + } + + public(friend) fun fee_cut_exists( + listing: &mut Listing, + ): bool { + df::exists_(&listing.id, FeeCutDfKey {}) + } + + /// Borrows `FeeCut` object mutably. + /// + /// Warning: This function is not safe to be shared, it does not check + /// for permissions and should ONLY be used by the function + /// `fee_cut::add_fee_cut` + public(friend) fun borrow_fee_cut_mut( + listing: &mut Listing, + ): &mut FeeCut { + df::borrow_mut(&mut listing.id, FeeCutDfKey {}) + } + /// Mutably borrow the listing's `Venue` /// /// `Venue` and inventories are unprotected therefore only market modules diff --git a/contracts/launchpad/sources/launchpad/proceeds.move b/contracts/launchpad/sources/launchpad/proceeds.move index 29acb2e4..21b9278b 100644 --- a/contracts/launchpad/sources/launchpad/proceeds.move +++ b/contracts/launchpad/sources/launchpad/proceeds.move @@ -18,6 +18,7 @@ module ob_launchpad::proceeds { friend ob_launchpad::flat_fee; friend ob_launchpad::listing; friend ob_launchpad::venue; + friend ob_launchpad::fee_cut; struct Proceeds has key, store { id: UID, @@ -150,7 +151,7 @@ module ob_launchpad::proceeds { // === Private Functions === - fun balance_mut( + public(friend) fun balance_mut( proceeds: &mut Proceeds, ): &mut Balance { df::borrow_mut( From e078a6b42cd9b2f540f3f935e18c835d715497e2 Mon Sep 17 00:00:00 2001 From: Nuno Boavida <45330362+nmboavida@users.noreply.github.com> Date: Fri, 29 Mar 2024 17:15:28 +0000 Subject: [PATCH 2/4] fee cut tests --- contracts/launchpad/tests/fees.move | 61 +++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/contracts/launchpad/tests/fees.move b/contracts/launchpad/tests/fees.move index d7bf7a5a..aef66ce9 100644 --- a/contracts/launchpad/tests/fees.move +++ b/contracts/launchpad/tests/fees.move @@ -6,11 +6,14 @@ module ob_launchpad::test_fees { use sui::test_scenario::{Self, ctx}; use ob_launchpad::flat_fee; + use ob_launchpad::fee_cut; use ob_launchpad::listing; use ob_launchpad::test_listing; const MARKETPLACE: address = @0xA123; const CREATOR: address = @0xA1C05; + const CUT_ADDR_1: address = @0x1; + const CUT_ADDR_2: address = @0x2; #[test] public fun marketplace_default_fee() { @@ -110,4 +113,62 @@ module ob_launchpad::test_fees { test_scenario::return_shared(listing); test_scenario::end(scenario); } + + #[test] + public fun test_fee_cut() { + let scenario = test_scenario::begin(MARKETPLACE); + + // Creates `Marketplace` with default fee + let (marketplace, listing) = test_listing::init_listing_and_marketplace( + CREATOR, MARKETPLACE, 2000, &mut scenario, + ); + + fee_cut::add_fee_cut( + &mut marketplace, + &mut listing, + CUT_ADDR_1, + 70, + ctx(&mut scenario) + ); + + fee_cut::add_fee_cut( + &mut marketplace, + &mut listing, + CUT_ADDR_2, + 30, + ctx(&mut scenario) + ); + + listing::pay(&mut listing, balance::create_for_testing(10_000), 1); + + flat_fee::collect_proceeds_and_fees( + &marketplace, &mut listing, ctx(&mut scenario), + ); + + test_scenario::next_tx(&mut scenario, MARKETPLACE); + + let marketplace_proceeds = + test_scenario::take_from_address>(&scenario, MARKETPLACE); + assert!(coin::value(&marketplace_proceeds) == 1_980, 0); + + test_scenario::return_to_address(MARKETPLACE, marketplace_proceeds); + + let creator_proceeds = + test_scenario::take_from_address>(&scenario, CREATOR); + assert!(coin::value(&creator_proceeds) == 7_920, 0); + + let addr1_proceeds = test_scenario::take_from_address>(&scenario, CUT_ADDR_1); + assert!(coin::value(&addr1_proceeds) == 70, 0); + + let addr2_proceeds = test_scenario::take_from_address>(&scenario, CUT_ADDR_2); + assert!(coin::value(&addr2_proceeds) == 30, 0); + + test_scenario::return_to_address(CREATOR, creator_proceeds); + test_scenario::return_to_address(CUT_ADDR_1, addr1_proceeds); + test_scenario::return_to_address(CUT_ADDR_2, addr2_proceeds); + + test_scenario::return_shared(marketplace); + test_scenario::return_shared(listing); + test_scenario::end(scenario); + } } From 003c9ef552520163413843d8a8878b49e83ea225 Mon Sep 17 00:00:00 2001 From: Nuno Boavida <45330362+nmboavida@users.noreply.github.com> Date: Tue, 2 Apr 2024 18:35:16 +0100 Subject: [PATCH 3/4] Added free mint module with sig verification --- contracts/launchpad/Move.lock | 7 +- .../sources/launchpad/discount/crypto.move | 92 +++++++++++++ .../sources/launchpad/discount/free_mint.move | 128 ++++++++++++++++++ .../launchpad/{ => discount}/rebate.move | 0 .../sources/launchpad/fees/fee_cut.move | 52 +++---- .../launchpad/sources/launchpad/listing.move | 27 ++++ .../sources/launchpad/market/fixed_price.move | 10 +- 7 files changed, 278 insertions(+), 38 deletions(-) create mode 100644 contracts/launchpad/sources/launchpad/discount/crypto.move create mode 100644 contracts/launchpad/sources/launchpad/discount/free_mint.move rename contracts/launchpad/sources/launchpad/{ => discount}/rebate.move (100%) diff --git a/contracts/launchpad/Move.lock b/contracts/launchpad/Move.lock index 56c40448..d508a657 100644 --- a/contracts/launchpad/Move.lock +++ b/contracts/launchpad/Move.lock @@ -2,7 +2,7 @@ [move] version = 0 -manifest_digest = "461044ADFC5BFF564B120D25764387C867ACD614C75866CF29CC57E3E5C0E117" +manifest_digest = "FB7BC10285E871AB700F3DC850EF22AB7AEB29A1EA8BCD02C80A1778E7635952" deps_digest = "397E6A9F7A624706DBDFEE056CE88391A15876868FD18A88504DA74EB458D697" dependencies = [ @@ -77,3 +77,8 @@ dependencies = [ { name = "Pseudorandom" }, { name = "Sui" }, ] + +[move.toolchain-version] +compiler-version = "1.21.1" +edition = "legacy" +flavor = "sui" diff --git a/contracts/launchpad/sources/launchpad/discount/crypto.move b/contracts/launchpad/sources/launchpad/discount/crypto.move new file mode 100644 index 00000000..b23b8b45 --- /dev/null +++ b/contracts/launchpad/sources/launchpad/discount/crypto.move @@ -0,0 +1,92 @@ +module ob_launchpad::crypto_utils { + use sui::ecdsa_k1; + use std::vector; + use std::debug::print; + use sui::object::{Self, ID}; + use sui::address as sui_address; + use sui::hash::keccak256; + + const EIncorrectSignature: u64 = 0; + + const KECCAK256: u8 = 0; + + public entry fun verify_message( + listing_id: ID, + addr: address, + nonce: u64, + addr_pubkey: vector, + signature: vector, + ) { + let msg = construct_msg( + object::id_to_bytes(&listing_id), + address_to_bytes(addr), + address_to_bytes(sui_address::from_u256((nonce as u256))), + ); + + assert!( + ecdsa_k1::secp256k1_verify(&signature, &addr_pubkey, &msg, KECCAK256), + EIncorrectSignature + ); + } + + // This function converts a public key to a Sui address using Keccak-256 for hashing. + // The public key is expected to be in the form of a vector (byte array). + public fun public_key_to_sui_address(public_key: vector): address { + let data_to_hash = vector[]; + + // Append the Ed25519 signature scheme flag byte (0x00) to the data to hash. + vector::push_back(&mut data_to_hash, 0x00); + + // Append the public key bytes to the data to hash. + let public_key_len = vector::length(&public_key); + let i = 0; + while (i < public_key_len) { + vector::push_back(&mut data_to_hash, *vector::borrow(&public_key, i)); + i = i + 1; + }; + + print(&data_to_hash); + + // Hash the data using Keccak-256. + sui_address::from_bytes(keccak256(&data_to_hash)) + } + + // This function converts a public key to a Sui address using Keccak-256 for hashing. + // The public key is expected to be in the form of a vector (byte array). + public fun public_key_to_sui_address_(public_key: vector): vector { + let data_to_hash = vector[]; + + // Append the Ed25519 signature scheme flag byte (0x00) to the data to hash. + vector::push_back(&mut data_to_hash, 0x00); + + // Append the public key bytes to the data to hash. + let public_key_len = vector::length(&public_key); + let i = 0; + while (i < public_key_len) { + vector::push_back(&mut data_to_hash, *vector::borrow(&public_key, i)); + i = i + 1; + }; + + print(&data_to_hash); + + // Hash the data using Keccak-256. + keccak256(&data_to_hash) + } + + public fun construct_msg( + listing_id: vector, + addr: vector, + nonce: vector, + ): vector { + let msg = vector::empty(); + vector::append(&mut msg, listing_id); + vector::append(&mut msg, addr); + vector::append(&mut msg, nonce); + + msg + } + + public fun address_to_bytes(addr: address): vector { + object::id_to_bytes(&object::id_from_address(addr)) + } +} diff --git a/contracts/launchpad/sources/launchpad/discount/free_mint.move b/contracts/launchpad/sources/launchpad/discount/free_mint.move new file mode 100644 index 00000000..f6575f33 --- /dev/null +++ b/contracts/launchpad/sources/launchpad/discount/free_mint.move @@ -0,0 +1,128 @@ +module ob_launchpad::free_mint { + use sui::tx_context::TxContext; + use sui::table::{Self, Table}; + use sui::object::{Self, ID}; + + use ob_launchpad::listing::{Self, Listing}; + use ob_launchpad::marketplace::Marketplace; + use ob_launchpad::crypto_utils; + + friend ob_launchpad::flat_fee; + friend ob_launchpad::fixed_price; + + const EInvalidAddress: u64 = 0; + + struct FreeMintList has store { + list: Table, + } + + struct State has store, drop { + // Bit flag: + // 1 --> Registered + // 2 --> Checked-in + // 3 --> Collected + phase: u8, + } + + struct FreeMintPotato { + // TODO: Should it be venue? + listing: ID, + addr: address, + } + + public fun add_free_mint_address( + marketplace: &Marketplace, + listing: &mut Listing, + addr: address, + ctx: &mut TxContext, + ) { + listing::assert_listing_marketplace_match(marketplace, listing); + listing::assert_correct_admin_or_member(marketplace, listing, ctx); + + let free_mint_exists = listing::free_mint_exists(listing); + + if (!free_mint_exists) { + listing::add_free_mint_internal(listing, FreeMintList { + list: table::new(ctx), + }); + }; + + let free_mint = listing::borrow_free_mint_mut(listing); + + assert!(!table::contains(&free_mint.list, addr), 0); + + let disc = State { phase: 1 }; + table::add(&mut free_mint.list, addr, disc); + } + + public fun collect_discount_with_sig( + listing: &mut Listing, + addr: address, + nonce: u64, + addr_pubkey: vector, + signature: vector, + ): FreeMintPotato { + // 1. Check if address is in the list + let free_mint = listing::borrow_free_mint_mut(listing); + + let state = table::borrow_mut(&mut free_mint.list, addr); + + assert!(state.phase == 1, 0); + + state.phase = 2; + + let listing_id = object::id(listing); + + crypto_utils::verify_message( + listing_id, + addr, + nonce, + addr_pubkey, + signature, + ); + + FreeMintPotato { listing: listing_id, addr } + } + + // To be called internally + public(friend) fun apply_free_mint_if_any( + listing: &mut Listing, + addr: address, + ): bool { + let free_mint_exists = listing::free_mint_exists(listing); + + if (!free_mint_exists) { + false + } else { + let free_mint = listing::borrow_free_mint_mut(listing); + + let on_the_list = table::contains(&free_mint.list, addr); + + if (!on_the_list) { + false + } else { + let state = table::borrow_mut(&mut free_mint.list, addr); + assert!(state.phase == 2, 0); + + state.phase = 3; + + true + } + } + } + + public fun checkout_free_mint( + listing: &mut Listing, + potato: FreeMintPotato, + ) { + let FreeMintPotato { listing: listing_id, addr } = potato; + + assert!(listing_id == object::id(listing), 0); + + let free_mint = listing::borrow_free_mint_mut(listing); + + let state = table::remove(&mut free_mint.list, addr); + + assert!(state.phase == 3, 0); + } +} diff --git a/contracts/launchpad/sources/launchpad/rebate.move b/contracts/launchpad/sources/launchpad/discount/rebate.move similarity index 100% rename from contracts/launchpad/sources/launchpad/rebate.move rename to contracts/launchpad/sources/launchpad/discount/rebate.move diff --git a/contracts/launchpad/sources/launchpad/fees/fee_cut.move b/contracts/launchpad/sources/launchpad/fees/fee_cut.move index 53b6cebd..001b8cf6 100644 --- a/contracts/launchpad/sources/launchpad/fees/fee_cut.move +++ b/contracts/launchpad/sources/launchpad/fees/fee_cut.move @@ -15,8 +15,8 @@ module ob_launchpad::fee_cut { const EInvalidAddress: u64 = 0; struct FeeCutList has store { - fee_cut: TableVec, - address_map: Table, + list: Table, + addresses: TableVec
, } struct Cut has store, drop { @@ -38,28 +38,19 @@ module ob_launchpad::fee_cut { if (!fee_cut_exists) { listing::add_fee_cut_internal(listing, FeeCutList { - fee_cut: table_vec::empty(ctx), - address_map: table::new(ctx), + list: table::new(ctx), + addresses: table_vec::empty(ctx), }); }; let fee_cut = listing::borrow_fee_cut_mut(listing); - if (!table::contains(&fee_cut.address_map, addr)) { - let addr_idx = table_vec::length(&fee_cut.fee_cut); - - table::add(&mut fee_cut.address_map, addr, addr_idx); - - let cut = Cut { addr, cut: cut_amount }; - - table_vec::push_back(&mut fee_cut.fee_cut, cut); + if (!table::contains(&fee_cut.list, addr)) { + table::add(&mut fee_cut.list, addr, cut_amount); + table_vec::push_back(&mut fee_cut.addresses, addr); } else { - let addr_idx = *table::borrow(&fee_cut.address_map, addr); - let fee_cut = table_vec::borrow_mut(&mut fee_cut.fee_cut, addr_idx); - - assert!(fee_cut.addr == addr, EInvalidAddress); - - fee_cut.cut = fee_cut.cut + cut_amount; + let cut = table::borrow_mut(&mut fee_cut.list, addr); + *cut = *cut + cut_amount; }; } @@ -75,40 +66,31 @@ module ob_launchpad::fee_cut { // Pay fee cuts if (fee_cut_exists) { - let to_transfer = table_vec::empty(ctx); - let fee_cut = listing::borrow_fee_cut_mut(listing); + let len = table_vec::length(&fee_cut.addresses); - let len = table_vec::length(&fee_cut.fee_cut); while (len > 0) { - table_vec::push_back(&mut to_transfer, table_vec::pop_back(&mut fee_cut.fee_cut)); + let fee_cut = listing::borrow_fee_cut_mut(listing); + let addr = table_vec::pop_back(&mut fee_cut.addresses); + let cut_amount = table::remove(&mut fee_cut.list, addr); - len = len - 1; - }; - - let proceeds = listing::borrow_proceeds_mut(listing); - let balance = proceeds::balance_mut(proceeds); - - let len = table_vec::length(&to_transfer); - while (len > 0) { - let cut_data = table_vec::pop_back(&mut to_transfer); + let proceeds = listing::borrow_proceeds_mut(listing); + let balance = proceeds::balance_mut(proceeds); let cut_balance = balance::split( balance, - cut_data.cut, + cut_amount, ); let cut = coin::from_balance(cut_balance, ctx); transfer::public_transfer( cut, - cut_data.addr, + addr, ); len = len - 1; }; - - table_vec::destroy_empty(to_transfer); }; } } diff --git a/contracts/launchpad/sources/launchpad/listing.move b/contracts/launchpad/sources/launchpad/listing.move index def3070d..41ce497e 100644 --- a/contracts/launchpad/sources/launchpad/listing.move +++ b/contracts/launchpad/sources/launchpad/listing.move @@ -55,6 +55,7 @@ module ob_launchpad::listing { friend ob_launchpad::flat_fee; friend ob_launchpad::fee_cut; + friend ob_launchpad::free_mint; friend ob_launchpad::market_whitelist; friend ob_launchpad::dutch_auction; friend ob_launchpad::fixed_price; @@ -131,6 +132,7 @@ module ob_launchpad::listing { struct WhitelistDfKey has store, copy, drop { venue_id: ID } struct StartSaleDfKey has store, copy, drop { venue_id: ID } struct FeeCutDfKey has store, copy, drop { } + struct DiscountDfKey has store, copy, drop { } // === Events === @@ -1235,6 +1237,31 @@ module ob_launchpad::listing { df::borrow_mut(&mut listing.id, FeeCutDfKey {}) } + /// Adds `DiscountList` object. We use a generic `DiscountList` to represent the + /// `DiscountList` type object to avoid dependency cycles. + /// + /// Warning: This function is not safe to be shared, it does not check + /// for permissions and should ONLY be used by the function + /// `discount::add_discount` + public(friend) fun add_free_mint_internal( + listing: &mut Listing, + discount_list: DiscountList + ) { + df::add(&mut listing.id, DiscountDfKey { }, discount_list); + } + + public(friend) fun free_mint_exists( + listing: &mut Listing, + ): bool { + df::exists_(&listing.id, DiscountDfKey {}) + } + + public(friend) fun borrow_free_mint_mut( + listing: &mut Listing, + ): &mut FeeCut { + df::borrow_mut(&mut listing.id, FeeCutDfKey {}) + } + /// Mutably borrow the listing's `Venue` /// /// `Venue` and inventories are unprotected therefore only market modules diff --git a/contracts/launchpad/sources/launchpad/market/fixed_price.move b/contracts/launchpad/sources/launchpad/market/fixed_price.move index 064abfb7..7e833d2e 100644 --- a/contracts/launchpad/sources/launchpad/market/fixed_price.move +++ b/contracts/launchpad/sources/launchpad/market/fixed_price.move @@ -18,6 +18,7 @@ module ob_launchpad::fixed_price { use ob_launchpad::venue::{Self, Venue}; use ob_launchpad::listing::{Self, Listing}; use ob_launchpad::market_whitelist::{Self, Certificate}; + use ob_launchpad::free_mint; /// Fixed price market object struct FixedPriceMarket has key, store { @@ -227,11 +228,16 @@ module ob_launchpad::fixed_price { balance: &mut Balance, ctx: &mut TxContext, ): T { + let sender = tx_context::sender(ctx); listing::apply_rebate(listing, balance); + let is_free_mint = free_mint::apply_free_mint_if_any( + listing, + sender, + ); let market = borrow_market(listing::borrow_venue(listing, venue_id)); + let price = if (is_free_mint) {0} else {market.price}; - let price = market.price; let inventory_id = market.inventory_id; listing::buy_pseudorandom_nft, MarketKey>( @@ -239,7 +245,7 @@ module ob_launchpad::fixed_price { MarketKey {}, inventory_id, venue_id, - tx_context::sender(ctx), + sender, balance::split(balance, price), ctx, ) From 340d8b8dc59ec9661109bb75ca9e1a1e08f2d111 Mon Sep 17 00:00:00 2001 From: Nuno Boavida <45330362+nmboavida@users.noreply.github.com> Date: Mon, 8 Apr 2024 14:05:33 +0100 Subject: [PATCH 4/4] Free Mint seggregated per venue ID --- .../sources/launchpad/discount/crypto.move | 4 +- .../sources/launchpad/discount/free_mint.move | 43 ++++++------ .../sources/launchpad/fees/fee_cut.move | 11 +--- .../launchpad/sources/launchpad/listing.move | 23 ++++--- .../sources/launchpad/market/fixed_price.move | 1 + contracts/launchpad/tests/fees.move | 65 ++++++++++++++++++- 6 files changed, 102 insertions(+), 45 deletions(-) diff --git a/contracts/launchpad/sources/launchpad/discount/crypto.move b/contracts/launchpad/sources/launchpad/discount/crypto.move index b23b8b45..cbcd9aa8 100644 --- a/contracts/launchpad/sources/launchpad/discount/crypto.move +++ b/contracts/launchpad/sources/launchpad/discount/crypto.move @@ -11,14 +11,14 @@ module ob_launchpad::crypto_utils { const KECCAK256: u8 = 0; public entry fun verify_message( - listing_id: ID, + venue_id: ID, addr: address, nonce: u64, addr_pubkey: vector, signature: vector, ) { let msg = construct_msg( - object::id_to_bytes(&listing_id), + object::id_to_bytes(&venue_id), address_to_bytes(addr), address_to_bytes(sui_address::from_u256((nonce as u256))), ); diff --git a/contracts/launchpad/sources/launchpad/discount/free_mint.move b/contracts/launchpad/sources/launchpad/discount/free_mint.move index f6575f33..f545fac1 100644 --- a/contracts/launchpad/sources/launchpad/discount/free_mint.move +++ b/contracts/launchpad/sources/launchpad/discount/free_mint.move @@ -1,7 +1,7 @@ module ob_launchpad::free_mint { use sui::tx_context::TxContext; use sui::table::{Self, Table}; - use sui::object::{Self, ID}; + use sui::object::ID; use ob_launchpad::listing::{Self, Listing}; use ob_launchpad::marketplace::Marketplace; @@ -10,8 +10,6 @@ module ob_launchpad::free_mint { friend ob_launchpad::flat_fee; friend ob_launchpad::fixed_price; - const EInvalidAddress: u64 = 0; - struct FreeMintList has store { list: Table, } @@ -25,29 +23,30 @@ module ob_launchpad::free_mint { } struct FreeMintPotato { - // TODO: Should it be venue? - listing: ID, + venue_id: ID, addr: address, } public fun add_free_mint_address( marketplace: &Marketplace, listing: &mut Listing, + venue_id: ID, addr: address, ctx: &mut TxContext, ) { listing::assert_listing_marketplace_match(marketplace, listing); listing::assert_correct_admin_or_member(marketplace, listing, ctx); + listing::assert_venue(listing, venue_id); - let free_mint_exists = listing::free_mint_exists(listing); + let free_mint_exists = listing::free_mint_exists(listing, venue_id); if (!free_mint_exists) { - listing::add_free_mint_internal(listing, FreeMintList { + listing::add_free_mint_internal(listing, venue_id, FreeMintList { list: table::new(ctx), }); }; - let free_mint = listing::borrow_free_mint_mut(listing); + let free_mint = listing::borrow_free_mint_mut(listing, venue_id); assert!(!table::contains(&free_mint.list, addr), 0); @@ -55,46 +54,47 @@ module ob_launchpad::free_mint { table::add(&mut free_mint.list, addr, disc); } - public fun collect_discount_with_sig( + public fun checkin_free_mint_with_sig( listing: &mut Listing, + venue_id: ID, addr: address, nonce: u64, addr_pubkey: vector, signature: vector, ): FreeMintPotato { - // 1. Check if address is in the list - let free_mint = listing::borrow_free_mint_mut(listing); + listing::assert_venue(listing, venue_id); + // 1. Check if address is in the list + let free_mint = listing::borrow_free_mint_mut(listing, venue_id); let state = table::borrow_mut(&mut free_mint.list, addr); assert!(state.phase == 1, 0); state.phase = 2; - let listing_id = object::id(listing); - crypto_utils::verify_message( - listing_id, + venue_id, addr, nonce, addr_pubkey, signature, ); - FreeMintPotato { listing: listing_id, addr } + FreeMintPotato { venue_id, addr } } - // To be called internally + // To be called internally. Does not assert venue ID as it's asserted upstream public(friend) fun apply_free_mint_if_any( listing: &mut Listing, + venue_id: ID, addr: address, ): bool { - let free_mint_exists = listing::free_mint_exists(listing); + let free_mint_exists = listing::free_mint_exists(listing, venue_id); if (!free_mint_exists) { false } else { - let free_mint = listing::borrow_free_mint_mut(listing); + let free_mint = listing::borrow_free_mint_mut(listing, venue_id); let on_the_list = table::contains(&free_mint.list, addr); @@ -115,11 +115,10 @@ module ob_launchpad::free_mint { listing: &mut Listing, potato: FreeMintPotato, ) { - let FreeMintPotato { listing: listing_id, addr } = potato; - - assert!(listing_id == object::id(listing), 0); + let FreeMintPotato { venue_id, addr } = potato; + listing::assert_venue(listing, venue_id); - let free_mint = listing::borrow_free_mint_mut(listing); + let free_mint = listing::borrow_free_mint_mut(listing, venue_id); let state = table::remove(&mut free_mint.list, addr); diff --git a/contracts/launchpad/sources/launchpad/fees/fee_cut.move b/contracts/launchpad/sources/launchpad/fees/fee_cut.move index 001b8cf6..cdcb464e 100644 --- a/contracts/launchpad/sources/launchpad/fees/fee_cut.move +++ b/contracts/launchpad/sources/launchpad/fees/fee_cut.move @@ -12,18 +12,11 @@ module ob_launchpad::fee_cut { friend ob_launchpad::flat_fee; - const EInvalidAddress: u64 = 0; - struct FeeCutList has store { list: Table, addresses: TableVec
, } - struct Cut has store, drop { - addr: address, - cut: u64 - } - public fun add_fee_cut( marketplace: &Marketplace, listing: &mut Listing, @@ -34,7 +27,7 @@ module ob_launchpad::fee_cut { listing::assert_listing_marketplace_match(marketplace, listing); listing::assert_correct_admin_or_member(marketplace, listing, ctx); - let fee_cut_exists = listing::fee_cut_exists(listing); + let fee_cut_exists = listing::fee_cut_exists(listing); if (!fee_cut_exists) { listing::add_fee_cut_internal(listing, FeeCutList { @@ -62,7 +55,7 @@ module ob_launchpad::fee_cut { listing::assert_listing_marketplace_match(marketplace, listing); listing::assert_correct_admin_or_member(marketplace, listing, ctx); - let fee_cut_exists = listing::fee_cut_exists(listing); + let fee_cut_exists = listing::fee_cut_exists(listing); // Pay fee cuts if (fee_cut_exists) { diff --git a/contracts/launchpad/sources/launchpad/listing.move b/contracts/launchpad/sources/launchpad/listing.move index 41ce497e..222c8ca5 100644 --- a/contracts/launchpad/sources/launchpad/listing.move +++ b/contracts/launchpad/sources/launchpad/listing.move @@ -132,7 +132,7 @@ module ob_launchpad::listing { struct WhitelistDfKey has store, copy, drop { venue_id: ID } struct StartSaleDfKey has store, copy, drop { venue_id: ID } struct FeeCutDfKey has store, copy, drop { } - struct DiscountDfKey has store, copy, drop { } + struct FreeMintDfKey has store, copy, drop { venue_id: ID} // === Events === @@ -1220,8 +1220,8 @@ module ob_launchpad::listing { df::add(&mut listing.id, FeeCutDfKey { }, fee_cut); } - public(friend) fun fee_cut_exists( - listing: &mut Listing, + public(friend) fun fee_cut_exists( + listing: &Listing, ): bool { df::exists_(&listing.id, FeeCutDfKey {}) } @@ -1245,21 +1245,24 @@ module ob_launchpad::listing { /// `discount::add_discount` public(friend) fun add_free_mint_internal( listing: &mut Listing, + venue_id: ID, discount_list: DiscountList ) { - df::add(&mut listing.id, DiscountDfKey { }, discount_list); + df::add(&mut listing.id, FreeMintDfKey { venue_id }, discount_list); } - public(friend) fun free_mint_exists( - listing: &mut Listing, + public(friend) fun free_mint_exists( + listing: &Listing, + venue_id: ID, ): bool { - df::exists_(&listing.id, DiscountDfKey {}) + df::exists_(&listing.id, FreeMintDfKey { venue_id }) } - public(friend) fun borrow_free_mint_mut( + public(friend) fun borrow_free_mint_mut( listing: &mut Listing, - ): &mut FeeCut { - df::borrow_mut(&mut listing.id, FeeCutDfKey {}) + venue_id: ID, + ): &mut DiscountList { + df::borrow_mut(&mut listing.id, FreeMintDfKey { venue_id }) } /// Mutably borrow the listing's `Venue` diff --git a/contracts/launchpad/sources/launchpad/market/fixed_price.move b/contracts/launchpad/sources/launchpad/market/fixed_price.move index 7e833d2e..5c980a4c 100644 --- a/contracts/launchpad/sources/launchpad/market/fixed_price.move +++ b/contracts/launchpad/sources/launchpad/market/fixed_price.move @@ -232,6 +232,7 @@ module ob_launchpad::fixed_price { listing::apply_rebate(listing, balance); let is_free_mint = free_mint::apply_free_mint_if_any( listing, + venue_id, sender, ); diff --git a/contracts/launchpad/tests/fees.move b/contracts/launchpad/tests/fees.move index aef66ce9..f9d5739d 100644 --- a/contracts/launchpad/tests/fees.move +++ b/contracts/launchpad/tests/fees.move @@ -124,7 +124,7 @@ module ob_launchpad::test_fees { ); fee_cut::add_fee_cut( - &mut marketplace, + &marketplace, &mut listing, CUT_ADDR_1, 70, @@ -132,7 +132,68 @@ module ob_launchpad::test_fees { ); fee_cut::add_fee_cut( - &mut marketplace, + &marketplace, + &mut listing, + CUT_ADDR_2, + 30, + ctx(&mut scenario) + ); + + listing::pay(&mut listing, balance::create_for_testing(10_000), 1); + + flat_fee::collect_proceeds_and_fees( + &marketplace, &mut listing, ctx(&mut scenario), + ); + + test_scenario::next_tx(&mut scenario, MARKETPLACE); + + let marketplace_proceeds = + test_scenario::take_from_address>(&scenario, MARKETPLACE); + assert!(coin::value(&marketplace_proceeds) == 1_980, 0); + + test_scenario::return_to_address(MARKETPLACE, marketplace_proceeds); + + let creator_proceeds = + test_scenario::take_from_address>(&scenario, CREATOR); + assert!(coin::value(&creator_proceeds) == 7_920, 0); + + let addr1_proceeds = test_scenario::take_from_address>(&scenario, CUT_ADDR_1); + assert!(coin::value(&addr1_proceeds) == 70, 0); + + let addr2_proceeds = test_scenario::take_from_address>(&scenario, CUT_ADDR_2); + assert!(coin::value(&addr2_proceeds) == 30, 0); + + test_scenario::return_to_address(CREATOR, creator_proceeds); + test_scenario::return_to_address(CUT_ADDR_1, addr1_proceeds); + test_scenario::return_to_address(CUT_ADDR_2, addr2_proceeds); + + test_scenario::return_shared(marketplace); + test_scenario::return_shared(listing); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = ob_launchpad::listing::EWrongListingOrMarketplaceAdmin)] + public fun test_fail_add_fee_cut_if_wrong_admin() { + let scenario = test_scenario::begin(MARKETPLACE); + + // Creates `Marketplace` with default fee + let (marketplace, listing) = test_listing::init_listing_and_marketplace( + CREATOR, MARKETPLACE, 2000, &mut scenario, + ); + + test_scenario::next_tx(&mut scenario, @0x999); + + fee_cut::add_fee_cut( + &marketplace, + &mut listing, + CUT_ADDR_1, + 70, + ctx(&mut scenario) + ); + + fee_cut::add_fee_cut( + &marketplace, &mut listing, CUT_ADDR_2, 30,