diff --git a/.gitignore b/.gitignore index f6ec567..653a781 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ flex_marketplace/.snfoundry_cache/ tool-versions .vscode/ +target \ No newline at end of file diff --git a/flex_marketplace/Scarb.lock b/flex_marketplace/Scarb.lock index d6edad7..08e907a 100644 --- a/flex_marketplace/Scarb.lock +++ b/flex_marketplace/Scarb.lock @@ -1,10 +1,16 @@ # Code generated by scarb DO NOT EDIT. version = 1 +[[package]] +name = "alexandria_storage" +version = "0.2.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?tag=cairo-v2.3.0-rc0#ae1d5149ff601a7ac5b39edc867d33ebd83d7f4f" + [[package]] name = "flex" version = "0.1.0" dependencies = [ + "alexandria_storage", "openzeppelin", "snforge_std", ] diff --git a/flex_marketplace/Scarb.toml b/flex_marketplace/Scarb.toml index d3f8580..7ef55d1 100644 --- a/flex_marketplace/Scarb.toml +++ b/flex_marketplace/Scarb.toml @@ -7,6 +7,7 @@ edition = "2023_01" starknet = "2.4.3" snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.14.0" } openzeppelin = { git = "https://github.com/openzeppelin/cairo-contracts", tag = "v0.8.0" } +alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git", tag = "cairo-v2.3.0-rc0" } [scripts] test = "snforge test" diff --git a/flex_marketplace/src/lib.cairo b/flex_marketplace/src/lib.cairo index 2ed09e9..f73ac15 100644 --- a/flex_marketplace/src/lib.cairo +++ b/flex_marketplace/src/lib.cairo @@ -36,11 +36,12 @@ mod marketplace { mod merkle; mod order_types; mod reentrancy_guard; + mod openedition; } + mod openedition; mod contract_deployer; mod currency_manager; - mod ERC721_flex; mod execution_manager; mod marketplace; mod proxy; diff --git a/flex_marketplace/src/marketplace/openedition.cairo b/flex_marketplace/src/marketplace/openedition.cairo new file mode 100644 index 0000000..daf437f --- /dev/null +++ b/flex_marketplace/src/marketplace/openedition.cairo @@ -0,0 +1,14 @@ +mod interfaces { + mod IFlexDrop; + mod IFlexDropContractMetadata; + mod INonFungibleFlexDropToken; +} + +mod ERC721_open_edition; + +mod FlexDrop; + +mod erc721_metadata { + mod ERC721_metadata; +} + diff --git a/flex_marketplace/src/marketplace/openedition/ERC721_open_edition.cairo b/flex_marketplace/src/marketplace/openedition/ERC721_open_edition.cairo new file mode 100644 index 0000000..59da6fa --- /dev/null +++ b/flex_marketplace/src/marketplace/openedition/ERC721_open_edition.cairo @@ -0,0 +1,364 @@ +#[starknet::contract] +mod ERC721 { + use alexandria_storage::list::ListTrait; + use openzeppelin::token::erc721::erc721::ERC721Component; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::security::reentrancyguard::ReentrancyGuardComponent; + use flex::marketplace::openedition::interfaces::IFlexDropContractMetadata::IFlexDropContractMetadata; + use flex::marketplace::openedition::erc721_metadata::ERC721_metadata::ERC721MetadataComponent; + use flex::marketplace::openedition::interfaces::IFlexDrop::{ + IFlexDropDispatcher, IFlexDropDispatcherTrait + }; + use flex::marketplace::openedition::interfaces::INonFungibleFlexDropToken::{ + INonFungibleFlexDropToken, I_NON_FUNGIBLE_FLEX_DROP_TOKEN_ID + }; + use flex::marketplace::utils::openedition::{PublicDrop, MultiConfigureStruct}; + use alexandria_storage::list::List; + use starknet::{ContractAddress, get_caller_address, get_contract_address}; + use integer::BoundedU64; + + component!(path: ERC721MetadataComponent, storage: erc721_metadata, event: ERC721MetadataEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: ERC721Component, storage: erc721, event: ERC721Event); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!( + path: ReentrancyGuardComponent, storage: reentrancy_guard, event: ReentrancyGuardEvent + ); + + #[abi(embed_v0)] + impl ERC721Impl = ERC721Component::ERC721Impl; + + #[abi(embed_v0)] + impl ERC721Metadata = ERC721Component::ERC721MetadataImpl; + + #[abi(embed_v0)] + impl ERC721FlexMetadataImpl = + ERC721MetadataComponent::FlexDropContractMetadataImpl; + + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + + impl ERC721MetadataInternalImpl = ERC721MetadataComponent::InternalImpl; + + impl ERC721InternalImpl = ERC721Component::InternalImpl; + + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + impl ReentrancyGuardInternalImpl = ReentrancyGuardComponent::InternalImpl; + + impl SRC5Internal = SRC5Component::InternalImpl; + + + #[storage] + struct Storage { + current_token_id: u256, + // mapping allowed FlexDrop contract + allowed_flex_drop: LegacyMap::, + total_minted: u64, + // mapping total minted per minter + total_minted_per_wallet: LegacyMap::, + // Track the enumerated allowed FlexDrop address + enumerated_allowed_flex_drop: List, + #[substorage(v0)] + erc721_metadata: ERC721MetadataComponent::Storage, + #[substorage(v0)] + erc721: ERC721Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + reentrancy_guard: ReentrancyGuardComponent::Storage + } + + #[constructor] + fn constructor( + ref self: ContractState, + owner: ContractAddress, + name: felt252, + symbol: felt252, + allowed_flex_drop: Array::, + ) { + self.erc721_metadata.initializer(owner); + self.erc721.initializer(name, symbol); + self.current_token_id.write(1); + + self.src5.register_interface(I_NON_FUNGIBLE_FLEX_DROP_TOKEN_ID); + + let mut enumerate_allowed_flex_drop = self.enumerated_allowed_flex_drop.read(); + enumerate_allowed_flex_drop.from_array(@allowed_flex_drop); + self.enumerated_allowed_flex_drop.write(enumerate_allowed_flex_drop); + + let allowed_flex_drop_length: u32 = allowed_flex_drop.len().try_into().unwrap(); + let mut index: u32 = 0; + loop { + if (index == allowed_flex_drop_length) { + break; + } + + let flex_drop = allowed_flex_drop.at(index); + self.allowed_flex_drop.write(*flex_drop, true); + index += 1; + }; + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + UpdateAllowedFlexDrop: UpdateAllowedFlexDrop, + #[flat] + ERC721MetadataEvent: ERC721MetadataComponent::Event, + #[flat] + ERC721Event: ERC721Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + ReentrancyGuardEvent: ReentrancyGuardComponent::Event + } + + #[derive(Drop, starknet::Event)] + struct UpdateAllowedFlexDrop { + new_flex_drop: Array::, + } + + #[abi(embed_v0)] + impl NonFungibleFlexDropTokenImpl of INonFungibleFlexDropToken { + // update FlexDrop contract addresses + fn update_allowed_flex_drop( + ref self: ContractState, allowed_flex_drop: Array:: + ) { + self.ownable.assert_only_owner(); + let mut enumerated_allowed_flex_drop = self.enumerated_allowed_flex_drop.read(); + let enumerated_allowed_flex_drop_length = enumerated_allowed_flex_drop.len(); + let new_allowed_flex_drop_length = allowed_flex_drop.len(); + + // Reset the old mapping. + let mut index_enumerate: u32 = 0; + let cp_enumerated_allowed = enumerated_allowed_flex_drop.array(); + loop { + if index_enumerate == enumerated_allowed_flex_drop_length { + break; + } + let old_allowed_flex_drop = cp_enumerated_allowed.at(index_enumerate); + self.allowed_flex_drop.write(*old_allowed_flex_drop, false); + index_enumerate += 1; + }; + + // Set the new mapping for allowed FlexDrop contracts. + let mut index_new_allowed: u32 = 0; + let cp_new_allowed = allowed_flex_drop.clone(); + loop { + if index_new_allowed == new_allowed_flex_drop_length { + break; + } + + self.allowed_flex_drop.write(*cp_new_allowed.at(index_new_allowed), true); + index_new_allowed += 1; + }; + + enumerated_allowed_flex_drop.from_array(@allowed_flex_drop); + self.enumerated_allowed_flex_drop.write(enumerated_allowed_flex_drop); + self.emit(UpdateAllowedFlexDrop { new_flex_drop: allowed_flex_drop }) + } + + // mint tokens, restricted to the FlexDrop contract + fn mint_flex_drop(ref self: ContractState, minter: ContractAddress, quantity: u64) { + self.reentrancy_guard.start(); + let flex_drop = get_caller_address(); + self.assert_allowed_flex_drop(flex_drop); + + assert( + self.get_total_minted() + quantity <= self.get_max_supply(), + 'Exceeds maximum total supply' + ); + + self.safe_mint_flex_drop(minter, quantity); + self.reentrancy_guard.end(); + } + + fn update_public_drop( + ref self: ContractState, flex_drop: ContractAddress, public_drop: PublicDrop + ) { + self.assert_owner_or_self(); + + self.assert_allowed_flex_drop(flex_drop); + + assert( + public_drop.start_time > 0 && public_drop.start_time + 3600 <= public_drop.end_time, + 'Wrong start and end time' + ); + + IFlexDropDispatcher { contract_address: flex_drop }.update_public_drop(public_drop); + } + + fn update_creator_payout( + ref self: ContractState, flex_drop: ContractAddress, payout_address: ContractAddress + ) { + self.assert_owner_or_self(); + + self.assert_allowed_flex_drop(flex_drop); + + IFlexDropDispatcher { contract_address: flex_drop } + .update_creator_payout_address(payout_address); + } + + fn update_fee_recipient( + ref self: ContractState, + flex_drop: ContractAddress, + fee_recipient: ContractAddress, + allowed: bool + ) { + self.assert_owner_or_self(); + + self.assert_allowed_flex_drop(flex_drop); + + IFlexDropDispatcher { contract_address: flex_drop } + .update_allowed_fee_recipient(fee_recipient, allowed) + } + + // update payer address for paying gas fee of minting NFT + fn update_payer( + ref self: ContractState, + flex_drop: ContractAddress, + payer: ContractAddress, + allowed: bool + ) { + self.assert_owner_or_self(); + + self.assert_allowed_flex_drop(flex_drop); + + IFlexDropDispatcher { contract_address: flex_drop }.update_payer(payer, allowed); + } + + fn multi_configure(ref self: ContractState, config: MultiConfigureStruct) { + self.ownable.assert_only_owner(); + + let mut max_supply = config.max_supply; + if max_supply == 0 { + max_supply = BoundedU64::max(); + } + + self.set_max_supply(max_supply); + self.set_base_uri(config.base_uri); + self.set_contract_uri(config.contract_uri); + + let public_drop = config.public_drop; + if public_drop.start_time != 0 && public_drop.end_time != 0 { + self.update_public_drop(config.flex_drop, public_drop); + } + + if !config.creator_payout_address.is_zero() { + self.update_creator_payout(config.flex_drop, config.creator_payout_address); + } + + if config.allowed_fee_recipients.len() > 0 { + let cp_allowed_fee_recipients = config.allowed_fee_recipients.clone(); + let mut index: u32 = 0; + loop { + if index == cp_allowed_fee_recipients.len() { + break; + } + self + .update_fee_recipient( + config.flex_drop, *cp_allowed_fee_recipients.at(index), true + ); + index += 1; + }; + } + + if config.disallowed_fee_recipients.len() > 0 { + let cp_disallowed_fee_recipients = config.disallowed_fee_recipients.clone(); + let mut index: u32 = 0; + loop { + if index == cp_disallowed_fee_recipients.len() { + break; + } + self + .update_fee_recipient( + config.flex_drop, *cp_disallowed_fee_recipients.at(index), false + ); + index += 1; + }; + } + + if config.allowed_payers.len() > 0 { + let cp_allowed_payers = config.allowed_payers.clone(); + let mut index: u32 = 0; + loop { + if index == cp_allowed_payers.len() { + break; + } + self.update_payer(config.flex_drop, *cp_allowed_payers.at(index), true); + index += 1; + }; + } + + if config.disallowed_payers.len() > 0 { + let cp_disallowed_payers = config.disallowed_payers.clone(); + let mut index: u32 = 0; + loop { + if index == cp_disallowed_payers.len() { + break; + } + self.update_payer(config.flex_drop, *cp_disallowed_payers.at(index), false); + index += 1; + }; + } + } + + // return (number minted, current total supply, max supply) + fn get_mint_state(self: @ContractState, minter: ContractAddress) -> (u64, u64, u64) { + let total_minted = self.total_minted_per_wallet.read(minter); + let current_total_supply = self.get_total_minted(); + let max_supply = self.get_max_supply(); + (total_minted, current_total_supply, max_supply) + } + + fn get_current_token_id(self: @ContractState) -> u256 { + self.current_token_id.read() + } + } + + #[generate_trait] + impl InternalFlexDropToken of InternalFlexDropTokenTrait { + fn safe_mint_flex_drop(ref self: ContractState, to: ContractAddress, quantity: u64) { + let mut current_token_id = self.get_current_token_id(); + let base_uri = self.get_base_uri(); + + self.current_token_id.write(current_token_id + quantity.into()); + self.total_minted.write(self.get_total_minted() + quantity); + + let mut index: u64 = 0; + loop { + if index == quantity { + break; + } + self.erc721._safe_mint(to, current_token_id, ArrayTrait::::new().span()); + self.erc721._set_token_uri(current_token_id, base_uri); + current_token_id += 1; + index += 1; + } + } + + fn assert_allowed_flex_drop(self: @ContractState, flex_drop: ContractAddress) { + assert(self.allowed_flex_drop.read(flex_drop), 'Only allowed FlexDrop'); + } + + fn get_total_minted(self: @ContractState) -> u64 { + self.total_minted.read() + } + + fn assert_owner_or_self(self: @ContractState) { + let caller = get_caller_address(); + assert( + caller == self.ownable.owner() || caller == get_contract_address(), 'Only owner' + ); + } + } +} diff --git a/flex_marketplace/src/marketplace/openedition/FlexDrop.cairo b/flex_marketplace/src/marketplace/openedition/FlexDrop.cairo new file mode 100644 index 0000000..9e68932 --- /dev/null +++ b/flex_marketplace/src/marketplace/openedition/FlexDrop.cairo @@ -0,0 +1,497 @@ +#[starknet::contract] +mod FlexDrop { + use core::box::BoxTrait; +use core::option::OptionTrait; + use flex::marketplace::utils::openedition::PublicDrop; + use flex::marketplace::openedition::interfaces::IFlexDrop::IFlexDrop; + use flex::marketplace::openedition::interfaces::INonFungibleFlexDropToken::{ + INonFungibleFlexDropTokenDispatcher, INonFungibleFlexDropTokenDispatcherTrait, + I_NON_FUNGIBLE_FLEX_DROP_TOKEN_ID + }; + use flex::marketplace::{ + currency_manager::{ICurrencyManagerDispatcher, ICurrencyManagerDispatcherTrait} + }; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::security::pausable::PausableComponent; + use openzeppelin::security::reentrancyguard::ReentrancyGuardComponent; + use openzeppelin::introspection::interface::{ISRC5Dispatcher, ISRC5DispatcherTrait}; + use alexandria_storage::list::{List, ListTrait}; + use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; + use array::{Array, ArrayTrait}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: PausableComponent, storage: pausable, event: PausableEvent); + component!(path: ReentrancyGuardComponent, storage: reentrancy, event: ReentrancyGuardEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + + #[abi(embed_v0)] + impl PausableImpl = PausableComponent::PausableImpl; + + impl ReentrancyGuardImpl = ReentrancyGuardComponent::InternalImpl; + + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + impl PausableInternalImpl = PausableComponent::InternalImpl; + + #[storage] + struct Storage { + // mapping nft address => Public drop + public_drops: LegacyMap::, + // mapping nft address => creator payout address + creator_payout_address: LegacyMap::, + // mapping (nft address, fee recipient) => is allowed + allowed_fee_recipients: LegacyMap::<(ContractAddress, ContractAddress), bool>, + // mapping nft address => enumerated allowed fee recipients + enumerated_allowed_fee_recipients: LegacyMap::>, + // mapping (nft address, payer) => is allowed + allowed_payer: LegacyMap::<(ContractAddress, ContractAddress), bool>, + // protocol fee + fee_bps: u128, + // mapping nft address => enumerated allowed payer + enumerated_allowed_payer: LegacyMap::>, + currency_manager: ICurrencyManagerDispatcher, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + pausable: PausableComponent::Storage, + #[substorage(v0)] + reentrancy: ReentrancyGuardComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + FlexDropMinted: FlexDropMinted, + ChangeFeeBPS: ChangeFeeBPS, + PublicDropUpdated: PublicDropUpdated, + CreatorPayoutUpdated: CreatorPayoutUpdated, + FeeRecipientUpdated: FeeRecipientUpdated, + PayerUpdated: PayerUpdated, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + PausableEvent: PausableComponent::Event, + #[flat] + ReentrancyGuardEvent: ReentrancyGuardComponent::Event, + } + + #[derive(Drop, starknet::Event)] + struct FlexDropMinted { + #[key] + nft_address: ContractAddress, + minter: ContractAddress, + fee_recipient: ContractAddress, + payer: ContractAddress, + quantity_minted: u64, + total_mint_price: u256, + fee_bps: u128, + } + + #[derive(Drop, starknet::Event)] + struct ChangeFeeBPS { + old_fee_bps: u128, + new_fee_bps: u128, + } + + #[derive(Drop, starknet::Event)] + struct PublicDropUpdated { + #[key] + nft_address: ContractAddress, + public_drop: PublicDrop, + } + + #[derive(Drop, starknet::Event)] + struct CreatorPayoutUpdated { + #[key] + nft_address: ContractAddress, + new_payout_address: ContractAddress, + } + + #[derive(Drop, starknet::Event)] + struct FeeRecipientUpdated { + #[key] + nft_address: ContractAddress, + fee_recipient: ContractAddress, + allowed: bool + } + + #[derive(Drop, starknet::Event)] + struct PayerUpdated { + #[key] + nft_address: ContractAddress, + payer: ContractAddress, + allowed: bool + } + + #[constructor] + fn constructor( + ref self: ContractState, + owner: ContractAddress, + currency_manager: ContractAddress, + fee_bps: u128, + ) { + self.ownable.initializer(owner); + + assert(fee_bps <= 10_000, 'Invalid Fee Basic Points'); + self.fee_bps.write(fee_bps); + self + .currency_manager + .write(ICurrencyManagerDispatcher { contract_address: currency_manager }); + } + + #[abi(embed_v0)] + impl FlexDropImpl of IFlexDrop { + fn mint_public( + ref self: ContractState, + nft_address: ContractAddress, + fee_recipient: ContractAddress, + minter_if_not_payer: ContractAddress, + quantity: u64, + currency: ContractAddress, + ) { + self.pausable.assert_not_paused(); + self.reentrancy.start(); + let public_drop = self.public_drops.read(nft_address); + self.assert_active_public_drop(@public_drop); + + self.assert_whitelisted_currency(@currency); + + let mut minter = get_caller_address(); + if !minter_if_not_payer.is_zero() { + minter = minter_if_not_payer.clone(); + } + + if minter != get_caller_address() { + self.assert_allowed_payer(nft_address, get_caller_address()); + } + + self + .assert_valid_mint_quantity( + @nft_address, @minter, quantity, public_drop.max_mint_per_wallet + ); + + self + .assert_allowed_fee_recipient( + @nft_address, @fee_recipient, public_drop.restrict_fee_recipients + ); + + let total_mint_price = quantity.into() * public_drop.mint_price; + self + .mint_and_pay( + nft_address, + get_caller_address(), + minter, + quantity, + currency, + total_mint_price, + self.fee_bps.read(), + fee_recipient + ); + self.reentrancy.end(); + } + + fn update_public_drop(ref self: ContractState, public_drop: PublicDrop) { + self.assert_only_non_fungible_flex_drop_token(); + + self.public_drops.write(get_caller_address(), public_drop); + self.emit(PublicDropUpdated { nft_address: get_caller_address(), public_drop }); + } + + fn update_creator_payout_address( + ref self: ContractState, new_payout_address: ContractAddress + ) { + self.assert_only_non_fungible_flex_drop_token(); + + assert(!new_payout_address.is_zero(), 'Only non zero payout address'); + self.creator_payout_address.write(get_caller_address(), new_payout_address); + + self + .emit( + CreatorPayoutUpdated { nft_address: get_caller_address(), new_payout_address } + ); + } + + fn update_allowed_fee_recipient( + ref self: ContractState, fee_recipient: ContractAddress, allowed: bool + ) { + self.assert_only_non_fungible_flex_drop_token(); + assert(!fee_recipient.is_zero(), 'Only non zero fee recipient'); + + let nft_address = get_caller_address(); + if allowed { + assert( + self.allowed_fee_recipients.read((nft_address, fee_recipient)), + 'Duplicate Fee Recipient' + ); + self.allowed_fee_recipients.write((nft_address, fee_recipient), true); + let mut enumerated_allowed_fee_recipients = self + .enumerated_allowed_fee_recipients + .read(nft_address); + enumerated_allowed_fee_recipients.append(fee_recipient); + self + .enumerated_allowed_fee_recipients + .write(nft_address, enumerated_allowed_fee_recipients); + } else { + assert( + !self.allowed_fee_recipients.read((nft_address, fee_recipient)), + 'Fee Recipient not present' + ); + self.allowed_fee_recipients.write((nft_address, fee_recipient), false); + self.remove_enumerated_allowed_fee_recipient(nft_address, fee_recipient); + } + + self.emit(FeeRecipientUpdated {nft_address, fee_recipient, allowed}); + } + + fn update_payer(ref self: ContractState, payer: ContractAddress, allowed: bool) { + self.assert_only_non_fungible_flex_drop_token(); + assert(!payer.is_zero(), 'Only non zero payer'); + + let nft_address = get_caller_address(); + if allowed { + assert(self.allowed_payer.read((nft_address, payer)), 'Duplicate payer'); + self.allowed_payer.write((nft_address, payer), true); + + let mut enumerated_allowed_payer = self.enumerated_allowed_payer.read(nft_address); + enumerated_allowed_payer.append(payer); + self.enumerated_allowed_payer.write(nft_address, enumerated_allowed_payer); + } else { + assert(!self.allowed_payer.read((nft_address, payer)), 'Payer not present'); + self.allowed_payer.write((nft_address, payer), false); + self.remove_enumerated_allowed_payer(nft_address, payer); + } + + self.emit(PayerUpdated {nft_address, payer, allowed}); + } + } + + #[abi(per_item)] + #[generate_trait] + impl AdditionalAccessors of AdditionalAccessorsTrait { + #[external(v0)] + fn pause(ref self: ContractState) { + self.ownable.assert_only_owner(); + self.pausable._pause(); + } + + #[external(v0)] + fn unpause(ref self: ContractState) { + self.ownable.assert_only_owner(); + self.pausable._unpause(); + } + + #[external(v0)] + fn change_currency_manager(ref self: ContractState, new_currency_manager: ContractAddress) { + self.ownable.assert_only_owner(); + self + .currency_manager + .write(ICurrencyManagerDispatcher { contract_address: new_currency_manager }) + } + + #[external(v0)] + fn change_fee_bps(ref self: ContractState, new_fee_bps: u128) { + self.ownable.assert_only_owner(); + + assert(new_fee_bps <= 10_000, 'Invalid Fee Basic Points'); + let old_fee_bps = self.fee_bps.read(); + self.fee_bps.write(new_fee_bps); + + self.emit(ChangeFeeBPS { old_fee_bps, new_fee_bps }) + } + + #[external(v0)] + fn get_fee_bps(self: @ContractState) -> u128 { + self.fee_bps.read() + } + + fn assert_only_non_fungible_flex_drop_token(self: @ContractState) { + let nft_address = get_caller_address(); + let is_supported_interface = ISRC5Dispatcher { contract_address: nft_address } + .supports_interface(I_NON_FUNGIBLE_FLEX_DROP_TOKEN_ID); + } + + fn assert_active_public_drop(self: @ContractState, public_drop: @PublicDrop) { + let block_time = get_block_timestamp(); + assert( + *public_drop.start_time <= block_time && *public_drop.end_time > block_time, + 'Public drop not active' + ); + } + + fn assert_whitelisted_currency(self: @ContractState, currency: @ContractAddress) { + assert( + self.currency_manager.read().is_currency_whitelisted(*currency), + 'currency is not whitelisted' + ); + } + + + fn assert_allowed_payer( + self: @ContractState, nft_address: ContractAddress, payer: ContractAddress + ) { + assert(self.allowed_payer.read((nft_address, payer)), 'Only allowed payer'); + } + + fn assert_valid_mint_quantity( + self: @ContractState, + nft_address: @ContractAddress, + minter: @ContractAddress, + quantity: u64, + max_total_mint_per_wallet: u64, + ) { + assert(quantity > 0, 'Only non zero quantity'); + + let (total_minted, current_total_supply, max_supply) = + INonFungibleFlexDropTokenDispatcher { + contract_address: *nft_address + } + .get_mint_state(*minter); + + assert( + total_minted + quantity <= max_total_mint_per_wallet, 'Exceeds maximum total minted' + ); + assert(quantity + current_total_supply <= max_supply, 'Exceeds maximum total supply'); + } + + fn assert_allowed_fee_recipient( + self: @ContractState, + nft_address: @ContractAddress, + fee_recipient: @ContractAddress, + restrict_fee_recipient: bool + ) { + assert(!(*fee_recipient).is_zero(), 'Only non zero fee recipient'); + + if restrict_fee_recipient { + assert( + self.allowed_fee_recipients.read((*nft_address, *fee_recipient)), + 'Only allowed fee recipient' + ); + } + } + + fn mint_and_pay( + ref self: ContractState, + nft_address: ContractAddress, + payer: ContractAddress, + minter: ContractAddress, + quantity: u64, + currency_address: ContractAddress, + total_mint_price: u256, + fee_bps: u128, + fee_recipient: ContractAddress + ) { + if total_mint_price != 0 { + self + .split_payout( + payer, + nft_address, + fee_recipient, + fee_bps, + currency_address, + total_mint_price + ); + } + + INonFungibleFlexDropTokenDispatcher { contract_address: nft_address } + .mint_flex_drop(minter, quantity); + + self + .emit( + FlexDropMinted { + nft_address, + minter, + fee_recipient, + payer, + quantity_minted: quantity, + total_mint_price, + fee_bps, + } + ) + } + + fn split_payout( + ref self: ContractState, + from: ContractAddress, + nft_address: ContractAddress, + fee_recipient: ContractAddress, + fee_bps: u128, + currency_address: ContractAddress, + total_mint_price: u256 + ) { + assert(fee_bps <= 10_000, 'Invalid Fee Basic Points'); + + let creator_payout_address = self.creator_payout_address.read(nft_address); + assert(!creator_payout_address.is_zero(), 'Only non zero creator payout'); + + let currency_contract = IERC20Dispatcher { contract_address: currency_address }; + if fee_bps == 0 { + currency_contract.transfer_from(from, creator_payout_address, total_mint_price); + return; + } + + let fee_amount = (total_mint_price * fee_bps.into()) / 10_000; + let payout_amount = total_mint_price - fee_amount; + + if fee_amount > 0 { + currency_contract.transfer_from(from, fee_recipient, fee_amount); + } + + currency_contract.transfer_from(from, creator_payout_address, payout_amount); + } + + fn remove_enumerated_allowed_fee_recipient( + ref self: ContractState, nft_address: ContractAddress, to_remove: ContractAddress + ) { + let mut enumerated_allowed_fee_recipients = self + .enumerated_allowed_fee_recipients + .read(nft_address); + + let mut index = 0; + let enumerated_allowed_fee_recipients_length = enumerated_allowed_fee_recipients.len(); + let mut new_enumerated_allowed_fee_recipients = ArrayTrait::::new(); + + let cp_enumerated = enumerated_allowed_fee_recipients.array(); + loop { + if index == enumerated_allowed_fee_recipients_length { + break; + } + + if *cp_enumerated.get(index).unwrap().unbox() != to_remove { + new_enumerated_allowed_fee_recipients.append(*cp_enumerated.get(index).unwrap().unbox()); + } + index += 1; + }; + enumerated_allowed_fee_recipients.from_array(@new_enumerated_allowed_fee_recipients); + self.enumerated_allowed_fee_recipients.write(nft_address, enumerated_allowed_fee_recipients); + } + + fn remove_enumerated_allowed_payer( + ref self: ContractState, nft_address: ContractAddress, to_remove: ContractAddress + ) { + let mut enumerated_allowed_payer = self + .enumerated_allowed_payer + .read(nft_address); + + let mut index = 0; + let enumerated_allowed_payer_length = enumerated_allowed_payer.len(); + let mut new_enumerated_allowed_payer = ArrayTrait::::new(); + + let cp_enumerated = enumerated_allowed_payer.array(); + loop { + if index == enumerated_allowed_payer_length { + break; + } + + if *cp_enumerated.get(index).unwrap().unbox() != to_remove { + new_enumerated_allowed_payer.append(*cp_enumerated.get(index).unwrap().unbox()); + } + index += 1; + }; + enumerated_allowed_payer.from_array(@new_enumerated_allowed_payer); + self.enumerated_allowed_payer.write(nft_address, enumerated_allowed_payer); + } + } +} diff --git a/flex_marketplace/src/marketplace/openedition/erc721_metadata/ERC721_metadata.cairo b/flex_marketplace/src/marketplace/openedition/erc721_metadata/ERC721_metadata.cairo new file mode 100644 index 0000000..ba84c44 --- /dev/null +++ b/flex_marketplace/src/marketplace/openedition/erc721_metadata/ERC721_metadata.cairo @@ -0,0 +1,88 @@ +#[starknet::component] +mod ERC721MetadataComponent { + use flex::marketplace::openedition::interfaces::IFlexDropContractMetadata::IFlexDropContractMetadata; + use starknet::{ContractAddress, get_caller_address}; + use integer::{U64PartialOrd, BoundedU64}; + + #[storage] + struct Storage { + max_supply: u64, + token_base_uri: felt252, + contract_uri: felt252, + owner: ContractAddress + } + + mod Errors { + const NOT_OWNER: felt252 = 'Caller is not the owner'; + const ZERO_ADDRESS_CALLER: felt252 = 'Caller is the zero address'; + const ZERO_ADDRESS_OWNER: felt252 = 'New owner is the zero address'; + } + + #[embeddable_as(FlexDropContractMetadataImpl)] + impl FlexDropContractMetadata< + TContractState, +HasComponent + > of IFlexDropContractMetadata> { + fn set_base_uri(ref self: ComponentState, new_token_uri: felt252) { + self.assert_only_owner(); + self._set_base_uri(new_token_uri); + } + + fn set_contract_uri(ref self: ComponentState, new_contract_uri: felt252) { + self.assert_only_owner(); + self._set_contract_uri(new_contract_uri); + } + + fn set_max_supply(ref self: ComponentState, new_max_supply: u64) { + self.assert_only_owner(); + self._set_max_supply(new_max_supply); + } + + fn get_base_uri(self: @ComponentState) -> felt252 { + self.token_base_uri.read() + } + + fn get_contract_uri(self: @ComponentState) -> felt252 { + self.contract_uri.read() + } + + fn get_max_supply(self: @ComponentState) -> u64 { + self.max_supply.read() + } + } + + #[generate_trait] + impl InternalImpl< + TContractState, +HasComponent + > of InternalTrait { + /// Sets the contract's initial owner. + /// + /// This function should be called at construction time. + fn initializer(ref self: ComponentState, owner: ContractAddress,) { + self.owner.write(owner); + } + + fn _set_base_uri(ref self: ComponentState, new_token_uri: felt252) { + self.token_base_uri.write(new_token_uri); + } + + fn _set_contract_uri(ref self: ComponentState, new_contract_uri: felt252) { + self.contract_uri.write(new_contract_uri); + } + + fn _set_max_supply(ref self: ComponentState, new_max_supply: u64) { + assert( + U64PartialOrd::lt(new_max_supply, BoundedU64::max()), + 'Cannot Exceed MaxSupply Of U64' + ); + self.max_supply.write(new_max_supply); + } + + fn assert_only_owner(self: @ComponentState) { + let owner: ContractAddress = self.owner.read(); + let caller: ContractAddress = get_caller_address(); + + assert(!caller.is_zero(), Errors::ZERO_ADDRESS_CALLER); + assert(caller == owner, Errors::NOT_OWNER); + } + } +} diff --git a/flex_marketplace/src/marketplace/openedition/interfaces/IFlexDrop.cairo b/flex_marketplace/src/marketplace/openedition/interfaces/IFlexDrop.cairo new file mode 100644 index 0000000..e16d9ad --- /dev/null +++ b/flex_marketplace/src/marketplace/openedition/interfaces/IFlexDrop.cairo @@ -0,0 +1,21 @@ +use starknet::ContractAddress; +use flex::marketplace::utils::openedition::PublicDrop; + +#[starknet::interface] +trait IFlexDrop { + fn mint_public( + ref self: TContractState, + nft_address: ContractAddress, + fee_recipient: ContractAddress, + minter_if_not_payer: ContractAddress, + quantity: u64, + currency: ContractAddress, + ); + fn update_public_drop(ref self: TContractState, public_drop: PublicDrop); + fn update_creator_payout_address(ref self: TContractState, new_payout_address: ContractAddress); + fn update_allowed_fee_recipient( + ref self: TContractState, fee_recipient: ContractAddress, allowed: bool + ); + fn update_payer(ref self: TContractState, payer: ContractAddress, allowed: bool); +} + diff --git a/flex_marketplace/src/marketplace/openedition/interfaces/IFlexDropContractMetadata.cairo b/flex_marketplace/src/marketplace/openedition/interfaces/IFlexDropContractMetadata.cairo new file mode 100644 index 0000000..2bda46a --- /dev/null +++ b/flex_marketplace/src/marketplace/openedition/interfaces/IFlexDropContractMetadata.cairo @@ -0,0 +1,9 @@ +#[starknet::interface] +trait IFlexDropContractMetadata { + fn set_base_uri(ref self: TContractState, new_token_uri: felt252); + fn set_contract_uri(ref self: TContractState, new_contract_uri: felt252); + fn set_max_supply(ref self: TContractState, new_max_supply: u64); + fn get_base_uri(self: @TContractState) -> felt252; + fn get_contract_uri(self: @TContractState) -> felt252; + fn get_max_supply(self: @TContractState) -> u64; +} diff --git a/flex_marketplace/src/marketplace/openedition/interfaces/INonFungibleFlexDropToken.cairo b/flex_marketplace/src/marketplace/openedition/interfaces/INonFungibleFlexDropToken.cairo new file mode 100644 index 0000000..f7609c7 --- /dev/null +++ b/flex_marketplace/src/marketplace/openedition/interfaces/INonFungibleFlexDropToken.cairo @@ -0,0 +1,36 @@ +use array::Array; +use starknet::ContractAddress; +use flex::marketplace::utils::openedition::{PublicDrop, MultiConfigureStruct}; + +const I_NON_FUNGIBLE_FLEX_DROP_TOKEN_ID: felt252 = + 0x7345179e748058b5caaabedd83ec1cce0034f037610dbd70846917635ea85a; + +#[starknet::interface] +trait INonFungibleFlexDropToken { + // update FlexDrop contract addresses + fn update_allowed_flex_drop( + ref self: TContractState, allowed_flex_drop: Array:: + ); + // mint tokens, restricted to the FlexDrop contract + fn mint_flex_drop(ref self: TContractState, minter: ContractAddress, quantity: u64); + fn update_public_drop( + ref self: TContractState, flex_drop: ContractAddress, public_drop: PublicDrop + ); + fn update_creator_payout( + ref self: TContractState, flex_drop: ContractAddress, payout_address: ContractAddress + ); + fn update_fee_recipient( + ref self: TContractState, + flex_drop: ContractAddress, + fee_recipient: ContractAddress, + allowed: bool + ); + // update payer address for paying gas fee of minting NFT + fn update_payer( + ref self: TContractState, flex_drop: ContractAddress, payer: ContractAddress, allowed: bool + ); + fn multi_configure(ref self: TContractState, config: MultiConfigureStruct); + // return (number minted, current total supply, max supply) + fn get_mint_state(self: @TContractState, minter: ContractAddress) -> (u64, u64, u64); + fn get_current_token_id(self: @TContractState) -> u256; +} diff --git a/flex_marketplace/src/marketplace/utils/openedition.cairo b/flex_marketplace/src/marketplace/utils/openedition.cairo new file mode 100644 index 0000000..25594ea --- /dev/null +++ b/flex_marketplace/src/marketplace/utils/openedition.cairo @@ -0,0 +1,25 @@ +use starknet::ContractAddress; +use array::Array; + +#[derive(Drop, Copy, Serde, starknet::Store)] +struct PublicDrop { + mint_price: u256, + start_time: u64, + end_time: u64, + max_mint_per_wallet: u64, + restrict_fee_recipients: bool, +} + +#[derive(Drop, Serde)] +struct MultiConfigureStruct { + max_supply: u64, + base_uri: felt252, + contract_uri: felt252, + flex_drop: ContractAddress, + public_drop: PublicDrop, + creator_payout_address: ContractAddress, + allowed_fee_recipients: Array::, + disallowed_fee_recipients: Array::, + allowed_payers: Array::, + disallowed_payers: Array::, +}