diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..43b2f18 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +starknet-foundry 0.27.0 +scarb 2.6.3 diff --git a/marketplace/.tool-versions b/marketplace/.tool-versions new file mode 100644 index 0000000..f239fe2 --- /dev/null +++ b/marketplace/.tool-versions @@ -0,0 +1 @@ +scarb 2.6.3 diff --git a/marketplace/Scarb.lock b/marketplace/Scarb.lock index 13a7d15..dc27f49 100644 --- a/marketplace/Scarb.lock +++ b/marketplace/Scarb.lock @@ -22,5 +22,5 @@ source = "git+https://github.com/openzeppelin/cairo-contracts?tag=v0.10.0#d77082 [[package]] name = "snforge_std" -version = "0.14.0" -source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.14.0#e8cbecee4e31ed428c76d5173eaa90c8df796fe3" +version = "0.26.0" +source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.26.0#50eb589db65e113efe4f09241feb59b574228c7e" diff --git a/marketplace/Scarb.toml b/marketplace/Scarb.toml index 4d7bf86..31f5366 100644 --- a/marketplace/Scarb.toml +++ b/marketplace/Scarb.toml @@ -5,7 +5,7 @@ edition = "2023_01" [dependencies] starknet = "2.6.3" -snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.14.0" } +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.26.0" } openzeppelin = { git = "https://github.com/openzeppelin/cairo-contracts", tag = "v0.10.0" } alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git", tag = "cairo-v2.3.0-rc0" } diff --git a/marketplace/Sell_any_item_at_fixed_price.md b/marketplace/Sell_any_item_at_fixed_price.md new file mode 100644 index 0000000..78be05f --- /dev/null +++ b/marketplace/Sell_any_item_at_fixed_price.md @@ -0,0 +1,33 @@ +## Strategy for Selling Any Item at a Fixed Price + +This contract, StrategySaleAnyItemAtFixedPrice, implements a strategy for selling items at a fixed price on StarkNet. The contract manages sales by allowing owners to list items (identified by token_id), set prices, handle buyer bids, and upgrade the contract. +Key Functionalities + + Initialization (constructor): + Initializes the contract with the owner's address and a protocol fee. The owner's address must not be zero, and the protocol fee is set during the initialization process. + + Updating Protocol Fee (update_protocol_fee): + Allows the contract owner to update the protocol fee, which is a fee applied to transactions executed within the marketplace. Only the contract owner has the authority to modify this fee. + + Getting Protocol Fee (protocol_fee): + Retrieves the current protocol fee stored in the contract. + + Setting Buy-Back Price for any item (set_buy_back_price_for_any_item): + Allows a buyer to set a buy-back price for any item from a specific collection. + + Executing Buyer Bids (can_execute_buyer_bid): + Verifies whether a buyer's bid can be executed. The contract checks if the token was listed for sale by the seller and compares the bid price with the existing buy-back price to determine if the bid is valid and executable. + + Contract Upgrading (upgrade): + Allows the contract owner to upgrade the contract's implementation using a new ClassHash. This operation can only be performed by the contract owner to ensure the contract's integrity. + +Events + + SetBuyBackPriceForItem: + Emitted when a buyer sets a buy-back price for an item. This event logs the buyer's address, the price set, and the collection address. + +Integrations + +This contract integrates with StarkNet's OwnableComponent for ownership management and UpgradeableComponent for upgradability. These integrations ensure that sensitive operations, such as updating protocol fees and upgrading the contract, are restricted to authorized users only. + +PR: https://github.com/Flex-NFT-Marketplace/Flex-Marketplace-Contract/pull/98 diff --git a/marketplace/src/strategy_any_item_from_collection_for_fixed_price.cairo b/marketplace/src/strategy_any_item_from_collection_for_fixed_price.cairo index 8b13789..868340b 100644 --- a/marketplace/src/strategy_any_item_from_collection_for_fixed_price.cairo +++ b/marketplace/src/strategy_any_item_from_collection_for_fixed_price.cairo @@ -1 +1,144 @@ +use starknet::ContractAddress; +use starknet::class_hash::ClassHash; +use marketplace::utils::order_types::{TakerOrder, MakerOrder, BuyerBidOrder}; +#[starknet::interface] +trait IStrategySaleAnyItemAtFixedPrice { + // fn initializer(ref self: TState, fee: u128, owner: ContractAddress); + fn update_protocol_fee(ref self: TState, fee: u128); + fn protocol_fee(self: @TState) -> u128; + // fn set_item_sale(ref self: TState, token_id: u128); + fn set_buy_back_price_for_item( + ref self: TState, price: u128, collection_address: ContractAddress + ); + fn can_execute_buyer_bid(self: @TState, buyer_bid: BuyerBidOrder) -> (bool, u128, u128); + fn upgrade(ref self: TState, impl_hash: ClassHash); +} + +#[feature("deprecated_legacy_map")] +#[starknet::contract] +mod StrategySaleAnyItemAtFixedPrice { + use starknet::{ContractAddress, contract_address_const, get_caller_address}; + use starknet::class_hash::ClassHash; + use starknet::get_block_timestamp; + + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::upgrades::{ + {UpgradeableComponent, interface::IUpgradeable}, + upgradeable::UpgradeableComponent::InternalTrait as UpgradeableInternalTrait + }; + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradable, event: UpgradeableEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + use marketplace::utils::order_types::{TakerOrder, MakerOrder, BuyerBidOrder}; + + #[derive(Debug, Drop, Copy, Serde, starknet::Store)] + pub struct BuyBack { + pub price: u128, + pub collection_address: ContractAddress + } + + #[storage] + struct Storage { + protocol_fee: u128, + item_for_sale: LegacyMap::< + u128, ContractAddress + >, // token_id: u128, seller_address:ContractAddress + buyer_bids: LegacyMap< + ContractAddress, BuyBack + >, // LegacyMap LegacyMap + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradable: UpgradeableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + SetBuyBackPriceForItem: SetBuyBackPriceForItem + } + + + #[derive(Drop, starknet::Event)] + pub struct SetBuyBackPriceForItem { + #[key] + pub buyer_address: ContractAddress, + pub price: u128, + pub collection_address: ContractAddress + } + + + #[constructor] + fn constructor(ref self: ContractState, owner: ContractAddress, fee: u128) { + assert(!owner.is_zero(), 'Owner cannot be a zero addr'); + self.ownable.initializer(owner); + self.protocol_fee.write(fee); + } + + #[abi(embed_v0)] + impl StrategySaleAnyItemAtFixedPriceImpl of super::IStrategySaleAnyItemAtFixedPrice< + ContractState + > { + fn update_protocol_fee(ref self: ContractState, fee: u128) { + self.ownable.assert_only_owner(); + self.protocol_fee.write(fee); + } + fn protocol_fee(self: @ContractState) -> u128 { + self.protocol_fee.read() + } + + fn set_buy_back_price_for_item( + ref self: ContractState, price: u128, collection_address: ContractAddress + ) { + let owner = get_caller_address(); + + let existing_buyer_bids = self.buyer_bids.read(owner); + + let buyer_token_price = existing_buyer_bids.price; + assert(buyer_token_price != price, 'Buy Back Price Set Already'); + + let new_buy_back_price = BuyBack { + price: price, collection_address: collection_address + }; + self.buyer_bids.write(owner, new_buy_back_price); + // emit an event + self + .emit( + SetBuyBackPriceForItem { + buyer_address: owner, price: price, collection_address: collection_address + } + ); + } + + fn can_execute_buyer_bid( + self: @ContractState, buyer_bid: BuyerBidOrder + ) -> (bool, u128, u128) { + let seller_item_listed_address = self.item_for_sale.read(buyer_bid.token_id); + let seller_address = get_caller_address(); + // check if seller has listed the token to be sold at any price + assert(seller_address == seller_item_listed_address, 'Not avaialable for sale'); + + // get the buyer token from the bid + let buyer_bids = self.buyer_bids.read(buyer_bid.buyer_adddress); + let buyer_token_price = buyer_bids.price; + if (buyer_token_price < 0) { + return (false, buyer_bid.token_id, buyer_bid.price); + } + return (true, buyer_bid.token_id, buyer_bid.price); + } + fn upgrade(ref self: ContractState, impl_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.upgradable._upgrade(impl_hash); + } + } +} diff --git a/marketplace/src/utils/order_types.cairo b/marketplace/src/utils/order_types.cairo index d66adf4..3bdde07 100644 --- a/marketplace/src/utils/order_types.cairo +++ b/marketplace/src/utils/order_types.cairo @@ -29,3 +29,10 @@ struct TakerOrder { min_percentage_to_ask: u128, // slippage protection (9000 = 90% of the final price must return to ask) params: felt252, } + +#[derive(Copy, Drop, Serde, Default)] +struct BuyerBidOrder { + token_id: u128, + buyer_adddress: ContractAddress, + price: u128 +}