From 51f35434d53762eba5cc3a7ae078f89811b2fc31 Mon Sep 17 00:00:00 2001 From: Brian Sin <briansinw3b@gmail.com> Date: Mon, 20 May 2024 10:45:11 +0700 Subject: [PATCH 1/3] feat/erc721-open-edition-multi-metadata --- .../src/marketplace/openedition.cairo | 4 + .../ERC721_open_edition_multi_metadata.cairo | 352 ++++++++++ .../marketplace/openedition/FlexDrop.cairo | 13 +- .../erc721/ERC721MultiMetadata.cairo | 629 ++++++++++++++++++ 4 files changed, 994 insertions(+), 4 deletions(-) create mode 100644 flex_marketplace/src/marketplace/openedition/ERC721_open_edition_multi_metadata.cairo create mode 100644 flex_marketplace/src/marketplace/openedition/erc721/ERC721MultiMetadata.cairo diff --git a/flex_marketplace/src/marketplace/openedition.cairo b/flex_marketplace/src/marketplace/openedition.cairo index e58fe95..d58a24c 100644 --- a/flex_marketplace/src/marketplace/openedition.cairo +++ b/flex_marketplace/src/marketplace/openedition.cairo @@ -7,13 +7,17 @@ mod interfaces { mod ERC721_open_edition; +mod ERC721_open_edition_multi_metadata; + mod FlexDrop; mod erc721 { mod ERC721; + mod ERC721MultiMetadata; } use erc721::ERC721; +use erc721::ERC721MultiMetadata; use interfaces::IFlexDrop::IFlexDrop; use interfaces::INonFungibleFlexDropToken::INonFungibleFlexDropToken; diff --git a/flex_marketplace/src/marketplace/openedition/ERC721_open_edition_multi_metadata.cairo b/flex_marketplace/src/marketplace/openedition/ERC721_open_edition_multi_metadata.cairo new file mode 100644 index 0000000..530a355 --- /dev/null +++ b/flex_marketplace/src/marketplace/openedition/ERC721_open_edition_multi_metadata.cairo @@ -0,0 +1,352 @@ +#[starknet::contract] +mod ERC721OpenEditionMultiMetadata { + use alexandria_storage::list::ListTrait; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::security::reentrancyguard::ReentrancyGuardComponent; + use flex::marketplace::openedition::ERC721MultiMetadata::ERC721MultiMetadataComponent; + 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::{PhaseDrop, MultiConfigureStruct}; + use alexandria_storage::list::List; + use starknet::{ContractAddress, get_caller_address, get_contract_address}; + use integer::BoundedU64; + + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: ERC721MultiMetadataComponent, storage: erc721, event: ERC721Event); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!( + path: ReentrancyGuardComponent, storage: reentrancy_guard, event: ReentrancyGuardEvent + ); + + #[abi(embed_v0)] + impl ERC721Impl = ERC721MultiMetadataComponent::ERC721Impl<ContractState>; + + #[abi(embed_v0)] + impl ERC721CamelImpl = + ERC721MultiMetadataComponent::ERC721CamelOnlyImpl<ContractState>; + + #[abi(embed_v0)] + impl ERC721MetadataImpl = + ERC721MultiMetadataComponent::ERC721MetadataImpl<ContractState>; + + #[abi(embed_v0)] + impl ERC721MetadataCamelImpl = + ERC721MultiMetadataComponent::ERC721MetadataCamelOnlyImpl<ContractState>; + + #[abi(embed_v0)] + impl FlexDropContractMetadataImpl = + ERC721MultiMetadataComponent::FlexDropContractMetadataImpl<ContractState>; + + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>; + + #[abi(embed_v0)] + impl SRC5CamelImple = SRC5Component::SRC5CamelImpl<ContractState>; + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl<ContractState>; + + impl ERC721InternalImpl = ERC721MultiMetadataComponent::InternalImpl<ContractState>; + + impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>; + + impl ReentrancyGuardInternalImpl = ReentrancyGuardComponent::InternalImpl<ContractState>; + + impl SRC5Internal = SRC5Component::InternalImpl<ContractState>; + + + #[storage] + struct Storage { + current_token_id: u256, + // mapping allowed FlexDrop contract + allowed_flex_drop: LegacyMap::<ContractAddress, bool>, + total_minted: u64, + // mapping total minted per minter + total_minted_per_wallet: LegacyMap::<ContractAddress, u64>, + // Track the enumerated allowed FlexDrop address + enumerated_allowed_flex_drop: List<ContractAddress>, + current_phase_id: u64, + #[substorage(v0)] + erc721: ERC721MultiMetadataComponent::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + reentrancy_guard: ReentrancyGuardComponent::Storage + } + + #[constructor] + fn constructor( + ref self: ContractState, + creator: ContractAddress, + name: ByteArray, + symbol: ByteArray, + token_base_uri: ByteArray, + allowed_flex_drop: Array::<ContractAddress>, + ) { + self.ownable.initializer(creator); + self.erc721.initializer(name, symbol, creator, token_base_uri); + self.current_token_id.write(1); + self.current_phase_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] + ERC721Event: ERC721MultiMetadataComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + ReentrancyGuardEvent: ReentrancyGuardComponent::Event + } + + #[derive(Drop, starknet::Event)] + struct UpdateAllowedFlexDrop { + new_flex_drop: Array::<ContractAddress>, + } + + #[abi(embed_v0)] + impl NonFungibleFlexDropTokenImpl of INonFungibleFlexDropToken<ContractState> { + // update FlexDrop contract addresses + fn update_allowed_flex_drop( + ref self: ContractState, allowed_flex_drop: Array::<ContractAddress> + ) { + 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 create_new_phase_drop( + ref self: ContractState, + flex_drop: ContractAddress, + phase_detail: PhaseDrop, + fee_recipient: ContractAddress, + ) { + self.ownable.assert_only_owner(); + self.assert_allowed_flex_drop(flex_drop); + let current_phase_id = self.current_phase_id.read(); + self.current_phase_id.write(current_phase_id + 1); + + IFlexDropDispatcher { contract_address: flex_drop } + .start_new_phase_drop(current_phase_id, phase_detail, fee_recipient) + } + + + fn update_phase_drop( + ref self: ContractState, + flex_drop: ContractAddress, + phase_id: u64, + phase_detail: PhaseDrop + ) { + self.assert_owner_or_self(); + + self.assert_allowed_flex_drop(flex_drop); + IFlexDropDispatcher { contract_address: flex_drop } + .update_phase_drop(phase_id, phase_detail); + } + + 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); + } + + // 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 { + self.set_max_supply(max_supply); + } + + if config.base_uri.len() > 0 { + self.set_base_uri(config.base_uri); + } + + if config.contract_uri.len() > 0 { + self.set_contract_uri(config.contract_uri); + } + + let phase_drop = config.phase_drop; + if phase_drop.phase_type != 0 + && phase_drop.start_time != 0 + && phase_drop.end_time != 0 { + if config.new_phase { + self.create_new_phase_drop(config.flex_drop, phase_drop, config.fee_recipient); + } else { + let current_id = self.current_phase_id.read(); + self.update_phase_drop(config.flex_drop, current_id, phase_drop); + } + } + + if !config.creator_payout_address.is_zero() { + self.update_creator_payout(config.flex_drop, config.creator_payout_address); + } + + 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(); + + self + .total_minted_per_wallet + .write(to, self.total_minted_per_wallet.read(to) + quantity); + 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::<felt252>::new().span()); + 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 index c4b97a2..3bb2758 100644 --- a/flex_marketplace/src/marketplace/openedition/FlexDrop.cairo +++ b/flex_marketplace/src/marketplace/openedition/FlexDrop.cairo @@ -171,8 +171,10 @@ mod FlexDrop { minter = minter_if_not_payer.clone(); } + let mut is_payer: bool = false; if minter != get_caller_address() { self.assert_allowed_payer(nft_address, get_caller_address()); + is_payer = true; } self @@ -188,6 +190,7 @@ mod FlexDrop { nft_address, get_caller_address(), minter, + is_payer, quantity, phase_drop.currency, total_mint_price, @@ -208,7 +211,7 @@ mod FlexDrop { let nft_address = get_caller_address(); let phase_detail = self.phase_drops.read((nft_address, phase_drop_id)); - assert!(phase_detail.phase_type == 0, "FlexDrop: Phase have not started"); + assert!(phase_detail.phase_type == 0, "FlexDrop: Phase have been started"); self.validate_new_phase_drop(@phase_drop); let new_phase_fee = self.new_phase_fee.read(); @@ -412,7 +415,7 @@ mod FlexDrop { assert!(*phase_drop.phase_type == 1, "FlexDrop: Currently supported public phase"); assert!( *phase_drop.start_time >= get_block_timestamp() - + 86400 && *phase_drop.start_time + + 1800 && *phase_drop.start_time + 3600 <= *phase_drop.end_time, "FlexDrop: Wrong start and end time" ); @@ -475,6 +478,7 @@ mod FlexDrop { nft_address: ContractAddress, payer: ContractAddress, minter: ContractAddress, + is_payer: bool, quantity: u64, currency_address: ContractAddress, total_mint_price: u256, @@ -482,7 +486,7 @@ mod FlexDrop { ) { self .split_payout( - payer, nft_address, fee_recipient, currency_address, total_mint_price + payer, is_payer, nft_address, fee_recipient, currency_address, total_mint_price ); INonFungibleFlexDropTokenDispatcher { contract_address: nft_address } @@ -505,6 +509,7 @@ mod FlexDrop { fn split_payout( ref self: ContractState, from: ContractAddress, + is_payer: bool, nft_address: ContractAddress, fee_recipient: ContractAddress, currency_address: ContractAddress, @@ -518,7 +523,7 @@ mod FlexDrop { fee_currency_contract.transfer_from(from, fee_recipient, fee_mint); } - if total_mint_price > 0 { + if total_mint_price > 0 && !is_payer { let currency_contract = IERC20Dispatcher { contract_address: currency_address }; let creator_payout_address = self.creator_payout_address.read(nft_address); assert!( diff --git a/flex_marketplace/src/marketplace/openedition/erc721/ERC721MultiMetadata.cairo b/flex_marketplace/src/marketplace/openedition/erc721/ERC721MultiMetadata.cairo new file mode 100644 index 0000000..611e5c0 --- /dev/null +++ b/flex_marketplace/src/marketplace/openedition/erc721/ERC721MultiMetadata.cairo @@ -0,0 +1,629 @@ +/// # ERC721 Component +/// +/// The ERC721 component provides implementations for the IERC721 interface, +/// the IERC721Metadata interface and IFlexDropContractMetadata interface. +#[starknet::component] +mod ERC721MultiMetadataComponent { + use core::byte_array::ByteArrayTrait; + use openzeppelin::account; + use openzeppelin::introspection::dual_src5::{DualCaseSRC5, DualCaseSRC5Trait}; + use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc721::dual721_receiver::{ + DualCaseERC721Receiver, DualCaseERC721ReceiverTrait + }; + use flex::marketplace::openedition::interfaces::IFlexDropContractMetadata; + use flex::marketplace::openedition::interfaces::IERC721; + use starknet::ContractAddress; + use starknet::get_caller_address; + use integer::{U64PartialOrd, BoundedU64}; + + + #[storage] + struct Storage { + ERC721_name: ByteArray, + ERC721_symbol: ByteArray, + ERC721_owners: LegacyMap<u256, ContractAddress>, + ERC721_balances: LegacyMap<ContractAddress, u256>, + ERC721_token_approvals: LegacyMap<u256, ContractAddress>, + ERC721_operator_approvals: LegacyMap<(ContractAddress, ContractAddress), bool>, + ERC721_base_uri: ByteArray, + ERC721_max_supply: u64, + ERC721_contract_uri: ByteArray, + ERC721_creator: ContractAddress + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Transfer: Transfer, + Approval: Approval, + ApprovalForAll: ApprovalForAll, + } + + /// Emitted when `token_id` token is transferred from `from` to `to`. + #[derive(Drop, starknet::Event)] + struct Transfer { + #[key] + from: ContractAddress, + #[key] + to: ContractAddress, + #[key] + token_id: u256 + } + + /// Emitted when `owner` enables `approved` to manage the `token_id` token. + #[derive(Drop, starknet::Event)] + struct Approval { + #[key] + owner: ContractAddress, + #[key] + approved: ContractAddress, + #[key] + token_id: u256 + } + + /// Emitted when `owner` enables or disables (`approved`) `operator` to manage + /// all of its assets. + #[derive(Drop, starknet::Event)] + struct ApprovalForAll { + #[key] + owner: ContractAddress, + #[key] + operator: ContractAddress, + approved: bool + } + + mod Errors { + const INVALID_TOKEN_ID: felt252 = 'ERC721: invalid token ID'; + const INVALID_ACCOUNT: felt252 = 'ERC721: invalid account'; + const UNAUTHORIZED: felt252 = 'ERC721: unauthorized caller'; + const APPROVAL_TO_OWNER: felt252 = 'ERC721: approval to owner'; + const SELF_APPROVAL: felt252 = 'ERC721: self approval'; + const INVALID_RECEIVER: felt252 = 'ERC721: invalid receiver'; + const ALREADY_MINTED: felt252 = 'ERC721: token already minted'; + const WRONG_SENDER: felt252 = 'ERC721: wrong sender'; + const SAFE_MINT_FAILED: felt252 = 'ERC721: safe mint failed'; + const SAFE_TRANSFER_FAILED: felt252 = 'ERC721: safe transfer failed'; + const NOT_CREATOR: felt252 = 'Caller is not the creator'; + const ZERO_ADDRESS_CALLER: felt252 = 'Caller is the zero address'; + const ZERO_ADDRESS_CREATOR: felt252 = 'New creator is the zero address'; + } + + // + // External + // + + #[embeddable_as(ERC721Impl)] + impl ERC721< + TContractState, + +HasComponent<TContractState>, + +SRC5Component::HasComponent<TContractState>, + +Drop<TContractState> + > of IERC721::IERC721<ComponentState<TContractState>> { + /// Returns the number of NFTs owned by `account`. + fn balance_of(self: @ComponentState<TContractState>, account: ContractAddress) -> u256 { + assert(!account.is_zero(), Errors::INVALID_ACCOUNT); + self.ERC721_balances.read(account) + } + + /// Returns the owner address of `token_id`. + /// + /// Requirements: + /// + /// - `token_id` exists. + fn owner_of(self: @ComponentState<TContractState>, token_id: u256) -> ContractAddress { + self._owner_of(token_id) + } + + /// Transfers ownership of `token_id` from `from` if `to` is either an account or `IERC721Receiver`. + /// + /// `data` is additional data, it has no specified format and it is sent in call to `to`. + /// + /// Requirements: + /// + /// - Caller is either approved or the `token_id` owner. + /// - `to` is not the zero address. + /// - `from` is not the zero address. + /// - `token_id` exists. + /// - `to` is either an account contract or supports the `IERC721Receiver` interface. + /// + /// Emits a `Transfer` event. + fn safe_transfer_from( + ref self: ComponentState<TContractState>, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + data: Span<felt252> + ) { + assert( + self._is_approved_or_owner(get_caller_address(), token_id), Errors::UNAUTHORIZED + ); + self._safe_transfer(from, to, token_id, data); + } + + /// Transfers ownership of `token_id` from `from` to `to`. + /// + /// Requirements: + /// + /// - Caller is either approved or the `token_id` owner. + /// - `to` is not the zero address. + /// - `from` is not the zero address. + /// - `token_id` exists. + /// + /// Emits a `Transfer` event. + fn transfer_from( + ref self: ComponentState<TContractState>, + from: ContractAddress, + to: ContractAddress, + token_id: u256 + ) { + assert( + self._is_approved_or_owner(get_caller_address(), token_id), Errors::UNAUTHORIZED + ); + self._transfer(from, to, token_id); + } + + /// Change or reaffirm the approved address for an NFT. + /// + /// Requirements: + /// + /// - The caller is either an approved operator or the `token_id` owner. + /// - `to` cannot be the token owner. + /// - `token_id` exists. + /// + /// Emits an `Approval` event. + fn approve(ref self: ComponentState<TContractState>, to: ContractAddress, token_id: u256) { + let owner = self._owner_of(token_id); + + let caller = get_caller_address(); + assert( + owner == caller || self.is_approved_for_all(owner, caller), Errors::UNAUTHORIZED + ); + self._approve(to, token_id); + } + + /// Enable or disable approval for `operator` to manage all of the + /// caller's assets. + /// + /// Requirements: + /// + /// - `operator` cannot be the caller. + /// + /// Emits an `Approval` event. + fn set_approval_for_all( + ref self: ComponentState<TContractState>, operator: ContractAddress, approved: bool + ) { + self._set_approval_for_all(get_caller_address(), operator, approved) + } + + /// Returns the address approved for `token_id`. + /// + /// Requirements: + /// + /// - `token_id` exists. + fn get_approved(self: @ComponentState<TContractState>, token_id: u256) -> ContractAddress { + assert(self._exists(token_id), Errors::INVALID_TOKEN_ID); + self.ERC721_token_approvals.read(token_id) + } + + /// Query if `operator` is an authorized operator for `owner`. + fn is_approved_for_all( + self: @ComponentState<TContractState>, owner: ContractAddress, operator: ContractAddress + ) -> bool { + self.ERC721_operator_approvals.read((owner, operator)) + } + } + + #[embeddable_as(ERC721MetadataImpl)] + impl ERC721Metadata< + TContractState, + +HasComponent<TContractState>, + +SRC5Component::HasComponent<TContractState>, + +Drop<TContractState> + > of IERC721::IERC721Metadata<ComponentState<TContractState>> { + /// Returns the NFT name. + fn name(self: @ComponentState<TContractState>) -> ByteArray { + self.ERC721_name.read() + } + + /// Returns the NFT symbol. + fn symbol(self: @ComponentState<TContractState>) -> ByteArray { + self.ERC721_symbol.read() + } + + /// Returns the Uniform Resource Identifier (URI) for the `token_id` token. + /// If the URI is not set for the `token_id`, the return value will be `0`. + /// + /// Requirements: + /// + /// - `token_id` exists. + fn token_uri(self: @ComponentState<TContractState>, token_id: u256) -> ByteArray { + assert(self._exists(token_id), Errors::INVALID_TOKEN_ID); + let base_uri = self._base_uri(); + if base_uri.len() == 0 { + return ""; + } else { + return format!("{}{}", base_uri, token_id); + } + } + } + + /// Adds camelCase support for `IERC721`. + #[embeddable_as(ERC721CamelOnlyImpl)] + impl ERC721CamelOnly< + TContractState, + +HasComponent<TContractState>, + +SRC5Component::HasComponent<TContractState>, + +Drop<TContractState> + > of IERC721::IERC721CamelOnly<ComponentState<TContractState>> { + fn balanceOf(self: @ComponentState<TContractState>, account: ContractAddress) -> u256 { + self.balance_of(account) + } + + fn ownerOf(self: @ComponentState<TContractState>, tokenId: u256) -> ContractAddress { + self.owner_of(tokenId) + } + + fn safeTransferFrom( + ref self: ComponentState<TContractState>, + from: ContractAddress, + to: ContractAddress, + tokenId: u256, + data: Span<felt252> + ) { + self.safe_transfer_from(from, to, tokenId, data) + } + + fn transferFrom( + ref self: ComponentState<TContractState>, + from: ContractAddress, + to: ContractAddress, + tokenId: u256 + ) { + self.transfer_from(from, to, tokenId) + } + + fn setApprovalForAll( + ref self: ComponentState<TContractState>, operator: ContractAddress, approved: bool + ) { + self.set_approval_for_all(operator, approved) + } + + fn getApproved(self: @ComponentState<TContractState>, tokenId: u256) -> ContractAddress { + self.get_approved(tokenId) + } + + fn isApprovedForAll( + self: @ComponentState<TContractState>, owner: ContractAddress, operator: ContractAddress + ) -> bool { + self.is_approved_for_all(owner, operator) + } + } + + /// Adds camelCase support for `IERC721Metadata`. + #[embeddable_as(ERC721MetadataCamelOnlyImpl)] + impl ERC721MetadataCamelOnly< + TContractState, + +HasComponent<TContractState>, + +SRC5Component::HasComponent<TContractState>, + +Drop<TContractState> + > of IERC721::IERC721MetadataCamelOnly<ComponentState<TContractState>> { + fn tokenURI(self: @ComponentState<TContractState>, tokenId: u256) -> ByteArray { + self.token_uri(tokenId) + } + } + + #[embeddable_as(FlexDropContractMetadataImpl)] + impl FlexDropContractMetadata< + TContractState, + +HasComponent<TContractState>, + +SRC5Component::HasComponent<TContractState>, + +Drop<TContractState> + > of IFlexDropContractMetadata::IFlexDropContractMetadata<ComponentState<TContractState>> { + fn set_base_uri(ref self: ComponentState<TContractState>, new_token_uri: ByteArray) { + self._assert_only_creator(); + self._set_base_uri(new_token_uri); + } + + fn set_contract_uri(ref self: ComponentState<TContractState>, new_contract_uri: ByteArray) { + self._assert_only_creator(); + self._set_contract_uri(new_contract_uri); + } + + fn set_max_supply(ref self: ComponentState<TContractState>, new_max_supply: u64) { + self._assert_only_creator(); + self._set_max_supply(new_max_supply); + } + + fn get_base_uri(self: @ComponentState<TContractState>) -> ByteArray { + self._base_uri() + } + + fn get_contract_uri(self: @ComponentState<TContractState>) -> ByteArray { + self._contract_uri() + } + + fn get_max_supply(self: @ComponentState<TContractState>) -> u64 { + self._get_max_supply() + } + } + + // + // Internal + // + + #[generate_trait] + impl InternalImpl< + TContractState, + +HasComponent<TContractState>, + impl SRC5: SRC5Component::HasComponent<TContractState>, + +Drop<TContractState> + > of InternalTrait<TContractState> { + /// Initializes the contract by setting the token name and symbol. + /// This should only be used inside the contract's constructor. + fn initializer( + ref self: ComponentState<TContractState>, + name: ByteArray, + symbol: ByteArray, + creator: ContractAddress, + token_base_uri: ByteArray + ) { + self.ERC721_creator.write(creator); + self.ERC721_name.write(name); + self.ERC721_symbol.write(symbol); + self.ERC721_base_uri.write(token_base_uri); + + let mut src5_component = get_dep_component_mut!(ref self, SRC5); + src5_component.register_interface(IERC721::IERC721_ID); + src5_component.register_interface(IERC721::IERC721_METADATA_ID); + } + + /// Returns the owner address of `token_id`. + /// + /// Requirements: + /// + /// - `token_id` exists. + fn _owner_of(self: @ComponentState<TContractState>, token_id: u256) -> ContractAddress { + let owner = self.ERC721_owners.read(token_id); + match owner.is_zero() { + bool::False(()) => owner, + bool::True(()) => panic_with_felt252(Errors::INVALID_TOKEN_ID) + } + } + + /// Returns whether `token_id` exists. + fn _exists(self: @ComponentState<TContractState>, token_id: u256) -> bool { + !self.ERC721_owners.read(token_id).is_zero() + } + + /// Returns whether `spender` is allowed to manage `token_id`. + /// + /// Requirements: + /// + /// - `token_id` exists. + fn _is_approved_or_owner( + self: @ComponentState<TContractState>, spender: ContractAddress, token_id: u256 + ) -> bool { + let owner = self._owner_of(token_id); + let is_approved_for_all = self.is_approved_for_all(owner, spender); + owner == spender || is_approved_for_all || spender == self.get_approved(token_id) + } + + /// Changes or reaffirms the approved address for an NFT. + /// + /// Internal function without access restriction. + /// + /// Requirements: + /// + /// - `token_id` exists. + /// - `to` is not the current token owner. + /// + /// Emits an `Approval` event. + fn _approve(ref self: ComponentState<TContractState>, to: ContractAddress, token_id: u256) { + let owner = self._owner_of(token_id); + assert(owner != to, Errors::APPROVAL_TO_OWNER); + + self.ERC721_token_approvals.write(token_id, to); + self.emit(Approval { owner, approved: to, token_id }); + } + + /// Enables or disables approval for `operator` to manage + /// all of the `owner` assets. + /// + /// Requirements: + /// + /// - `operator` cannot be the caller. + /// + /// Emits an `Approval` event. + fn _set_approval_for_all( + ref self: ComponentState<TContractState>, + owner: ContractAddress, + operator: ContractAddress, + approved: bool + ) { + assert(owner != operator, Errors::SELF_APPROVAL); + self.ERC721_operator_approvals.write((owner, operator), approved); + self.emit(ApprovalForAll { owner, operator, approved }); + } + + /// Mints `token_id` and transfers it to `to`. + /// Internal function without access restriction. + /// + /// Requirements: + /// + /// - `to` is not the zero address. + /// - `token_id` does not exist. + /// + /// Emits a `Transfer` event. + fn _mint(ref self: ComponentState<TContractState>, to: ContractAddress, token_id: u256) { + assert(!to.is_zero(), Errors::INVALID_RECEIVER); + assert(!self._exists(token_id), Errors::ALREADY_MINTED); + + self.ERC721_balances.write(to, self.ERC721_balances.read(to) + 1); + self.ERC721_owners.write(token_id, to); + + self.emit(Transfer { from: Zeroable::zero(), to, token_id }); + } + + /// Transfers `token_id` from `from` to `to`. + /// + /// Internal function without access restriction. + /// + /// Requirements: + /// + /// - `to` is not the zero address. + /// - `from` is the token owner. + /// - `token_id` exists. + /// + /// Emits a `Transfer` event. + fn _transfer( + ref self: ComponentState<TContractState>, + from: ContractAddress, + to: ContractAddress, + token_id: u256 + ) { + assert(!to.is_zero(), Errors::INVALID_RECEIVER); + let owner = self._owner_of(token_id); + assert(from == owner, Errors::WRONG_SENDER); + + // Implicit clear approvals, no need to emit an event + self.ERC721_token_approvals.write(token_id, Zeroable::zero()); + + self.ERC721_balances.write(from, self.ERC721_balances.read(from) - 1); + self.ERC721_balances.write(to, self.ERC721_balances.read(to) + 1); + self.ERC721_owners.write(token_id, to); + + self.emit(Transfer { from, to, token_id }); + } + + /// Destroys `token_id`. The approval is cleared when the token is burned. + /// + /// This internal function does not check if the caller is authorized + /// to operate on the token. + /// + /// Requirements: + /// + /// - `token_id` exists. + /// + /// Emits a `Transfer` event. + fn _burn(ref self: ComponentState<TContractState>, token_id: u256) { + let owner = self._owner_of(token_id); + + // Implicit clear approvals, no need to emit an event + self.ERC721_token_approvals.write(token_id, Zeroable::zero()); + + self.ERC721_balances.write(owner, self.ERC721_balances.read(owner) - 1); + self.ERC721_owners.write(token_id, Zeroable::zero()); + + self.emit(Transfer { from: owner, to: Zeroable::zero(), token_id }); + } + + /// Mints `token_id` if `to` is either an account or `IERC721Receiver`. + /// + /// `data` is additional data, it has no specified format and it is sent in call to `to`. + /// + /// Requirements: + /// + /// - `token_id` does not exist. + /// - `to` is either an account contract or supports the `IERC721Receiver` interface. + /// + /// Emits a `Transfer` event. + fn _safe_mint( + ref self: ComponentState<TContractState>, + to: ContractAddress, + token_id: u256, + data: Span<felt252> + ) { + self._mint(to, token_id); + assert( + _check_on_erc721_received(Zeroable::zero(), to, token_id, data), + Errors::SAFE_MINT_FAILED + ); + } + + /// Transfers ownership of `token_id` from `from` if `to` is either an account or `IERC721Receiver`. + /// + /// `data` is additional data, it has no specified format and it is sent in call to `to`. + /// + /// Requirements: + /// + /// - `to` cannot be the zero address. + /// - `from` must be the token owner. + /// - `token_id` exists. + /// - `to` is either an account contract or supports the `IERC721Receiver` interface. + /// + /// Emits a `Transfer` event. + fn _safe_transfer( + ref self: ComponentState<TContractState>, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + data: Span<felt252> + ) { + self._transfer(from, to, token_id); + assert( + _check_on_erc721_received(from, to, token_id, data), Errors::SAFE_TRANSFER_FAILED + ); + } + + /// Sets the base URI. + fn _set_base_uri(ref self: ComponentState<TContractState>, base_uri: ByteArray) { + self.ERC721_base_uri.write(base_uri); + } + + /// Base URI for computing `token_uri`. + /// + /// If set, the resulting URI for each token will be the concatenation of the base URI and the token ID. + /// Returns an empty `ByteArray` if not set. + fn _base_uri(self: @ComponentState<TContractState>) -> ByteArray { + self.ERC721_base_uri.read() + } + + fn _set_contract_uri( + ref self: ComponentState<TContractState>, new_contract_uri: ByteArray + ) { + self.ERC721_contract_uri.write(new_contract_uri); + } + + fn _contract_uri(self: @ComponentState<TContractState>) -> ByteArray { + self.ERC721_contract_uri.read() + } + + fn _set_max_supply(ref self: ComponentState<TContractState>, new_max_supply: u64) { + assert( + U64PartialOrd::lt(new_max_supply, BoundedU64::max()), + 'Cannot Exceed MaxSupply Of U64' + ); + self.ERC721_max_supply.write(new_max_supply); + } + + fn _get_max_supply(self: @ComponentState<TContractState>) -> u64 { + self.ERC721_max_supply.read() + } + + fn _assert_only_creator(self: @ComponentState<TContractState>) { + let creator: ContractAddress = self.ERC721_creator.read(); + let caller: ContractAddress = get_caller_address(); + + assert(!caller.is_zero(), Errors::ZERO_ADDRESS_CALLER); + assert(caller == creator, Errors::NOT_CREATOR); + } + } + + /// Checks if `to` either is an account contract or has registered support + /// for the `IERC721Receiver` interface through SRC5. + fn _check_on_erc721_received( + from: ContractAddress, to: ContractAddress, token_id: u256, data: Span<felt252> + ) -> bool { + if (DualCaseSRC5 { contract_address: to } + .supports_interface(IERC721::IERC721_RECEIVER_ID)) { + DualCaseERC721Receiver { contract_address: to } + .on_erc721_received( + get_caller_address(), from, token_id, data + ) == IERC721::IERC721_RECEIVER_ID + } else { + DualCaseSRC5 { contract_address: to }.supports_interface(account::interface::ISRC6_ID) + } + } +} From 792983505f873d2ced305a50c1bb7d95e3cc0989 Mon Sep 17 00:00:00 2001 From: Brian Sin <briansinw3b@gmail.com> Date: Wed, 22 May 2024 14:04:00 +0700 Subject: [PATCH 2/3] remove total supply of ERC721 --- .../openedition/ERC721_open_edition.cairo | 22 +++++++------------ .../ERC721_open_edition_multi_metadata.cairo | 21 ++++++------------ .../marketplace/openedition/FlexDrop.cairo | 4 +--- .../openedition/erc721/ERC721.cairo | 22 ------------------- .../erc721/ERC721MultiMetadata.cairo | 22 ------------------- .../IFlexDropContractMetadata.cairo | 2 -- .../INonFungibleFlexDropToken.cairo | 3 ++- .../src/marketplace/utils/openedition.cairo | 1 - 8 files changed, 18 insertions(+), 79 deletions(-) diff --git a/flex_marketplace/src/marketplace/openedition/ERC721_open_edition.cairo b/flex_marketplace/src/marketplace/openedition/ERC721_open_edition.cairo index 057305f..6cc192a 100644 --- a/flex_marketplace/src/marketplace/openedition/ERC721_open_edition.cairo +++ b/flex_marketplace/src/marketplace/openedition/ERC721_open_edition.cairo @@ -1,5 +1,6 @@ #[starknet::contract] mod ERC721OpenEdition { + use core::array::ArrayTrait; use alexandria_storage::list::ListTrait; use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::introspection::src5::SRC5Component; @@ -177,11 +178,6 @@ mod ERC721OpenEdition { 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(); } @@ -243,11 +239,6 @@ mod ERC721OpenEdition { fn multi_configure(ref self: ContractState, config: MultiConfigureStruct) { self.ownable.assert_only_owner(); - let mut max_supply = config.max_supply; - if max_supply != 0 { - self.set_max_supply(max_supply); - } - if config.base_uri.len() > 0 { self.set_base_uri(config.base_uri); } @@ -297,17 +288,20 @@ mod ERC721OpenEdition { } } - // return (number minted, current total supply, max supply) - fn get_mint_state(self: @ContractState, minter: ContractAddress) -> (u64, u64, u64) { + // return (number minted, current total supply) + fn get_mint_state(self: @ContractState, minter: ContractAddress) -> (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) + (total_minted, current_total_supply) } fn get_current_token_id(self: @ContractState) -> u256 { self.current_token_id.read() } + + fn get_allowed_flex_drops(self: @ContractState) -> Span::<ContractAddress> { + self.enumerated_allowed_flex_drop.read().array().span() + } } #[generate_trait] diff --git a/flex_marketplace/src/marketplace/openedition/ERC721_open_edition_multi_metadata.cairo b/flex_marketplace/src/marketplace/openedition/ERC721_open_edition_multi_metadata.cairo index 530a355..aa2269d 100644 --- a/flex_marketplace/src/marketplace/openedition/ERC721_open_edition_multi_metadata.cairo +++ b/flex_marketplace/src/marketplace/openedition/ERC721_open_edition_multi_metadata.cairo @@ -179,11 +179,6 @@ mod ERC721OpenEditionMultiMetadata { 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(); } @@ -245,11 +240,6 @@ mod ERC721OpenEditionMultiMetadata { fn multi_configure(ref self: ContractState, config: MultiConfigureStruct) { self.ownable.assert_only_owner(); - let mut max_supply = config.max_supply; - if max_supply != 0 { - self.set_max_supply(max_supply); - } - if config.base_uri.len() > 0 { self.set_base_uri(config.base_uri); } @@ -299,17 +289,20 @@ mod ERC721OpenEditionMultiMetadata { } } - // return (number minted, current total supply, max supply) - fn get_mint_state(self: @ContractState, minter: ContractAddress) -> (u64, u64, u64) { + // return (number minted, current total supply) + fn get_mint_state(self: @ContractState, minter: ContractAddress) -> (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) + (total_minted, current_total_supply) } fn get_current_token_id(self: @ContractState) -> u256 { self.current_token_id.read() } + + fn get_allowed_flex_drops(self: @ContractState) -> Span::<ContractAddress> { + self.enumerated_allowed_flex_drop.read().array().span() + } } #[generate_trait] diff --git a/flex_marketplace/src/marketplace/openedition/FlexDrop.cairo b/flex_marketplace/src/marketplace/openedition/FlexDrop.cairo index 3bb2758..30ca8e5 100644 --- a/flex_marketplace/src/marketplace/openedition/FlexDrop.cairo +++ b/flex_marketplace/src/marketplace/openedition/FlexDrop.cairo @@ -454,8 +454,7 @@ mod FlexDrop { ) { assert(quantity > 0, 'Only non zero quantity'); - let (total_minted, current_total_supply, max_supply) = - INonFungibleFlexDropTokenDispatcher { + let (total_minted, _) = INonFungibleFlexDropTokenDispatcher { contract_address: *nft_address } .get_mint_state(*minter); @@ -463,7 +462,6 @@ mod FlexDrop { 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, fee_recipient: @ContractAddress,) { diff --git a/flex_marketplace/src/marketplace/openedition/erc721/ERC721.cairo b/flex_marketplace/src/marketplace/openedition/erc721/ERC721.cairo index 4bca428..492b22f 100644 --- a/flex_marketplace/src/marketplace/openedition/erc721/ERC721.cairo +++ b/flex_marketplace/src/marketplace/openedition/erc721/ERC721.cairo @@ -28,7 +28,6 @@ mod ERC721Component { ERC721_token_approvals: LegacyMap<u256, ContractAddress>, ERC721_operator_approvals: LegacyMap<(ContractAddress, ContractAddress), bool>, ERC721_base_uri: ByteArray, - ERC721_max_supply: u64, ERC721_contract_uri: ByteArray, ERC721_creator: ContractAddress } @@ -331,11 +330,6 @@ mod ERC721Component { self._set_contract_uri(new_contract_uri); } - fn set_max_supply(ref self: ComponentState<TContractState>, new_max_supply: u64) { - self._assert_only_creator(); - self._set_max_supply(new_max_supply); - } - fn get_base_uri(self: @ComponentState<TContractState>) -> ByteArray { self._base_uri() } @@ -343,10 +337,6 @@ mod ERC721Component { fn get_contract_uri(self: @ComponentState<TContractState>) -> ByteArray { self._contract_uri() } - - fn get_max_supply(self: @ComponentState<TContractState>) -> u64 { - self._get_max_supply() - } } // @@ -590,18 +580,6 @@ mod ERC721Component { self.ERC721_contract_uri.read() } - fn _set_max_supply(ref self: ComponentState<TContractState>, new_max_supply: u64) { - assert( - U64PartialOrd::lt(new_max_supply, BoundedU64::max()), - 'Cannot Exceed MaxSupply Of U64' - ); - self.ERC721_max_supply.write(new_max_supply); - } - - fn _get_max_supply(self: @ComponentState<TContractState>) -> u64 { - self.ERC721_max_supply.read() - } - fn _assert_only_creator(self: @ComponentState<TContractState>) { let creator: ContractAddress = self.ERC721_creator.read(); let caller: ContractAddress = get_caller_address(); diff --git a/flex_marketplace/src/marketplace/openedition/erc721/ERC721MultiMetadata.cairo b/flex_marketplace/src/marketplace/openedition/erc721/ERC721MultiMetadata.cairo index 611e5c0..8212bff 100644 --- a/flex_marketplace/src/marketplace/openedition/erc721/ERC721MultiMetadata.cairo +++ b/flex_marketplace/src/marketplace/openedition/erc721/ERC721MultiMetadata.cairo @@ -28,7 +28,6 @@ mod ERC721MultiMetadataComponent { ERC721_token_approvals: LegacyMap<u256, ContractAddress>, ERC721_operator_approvals: LegacyMap<(ContractAddress, ContractAddress), bool>, ERC721_base_uri: ByteArray, - ERC721_max_supply: u64, ERC721_contract_uri: ByteArray, ERC721_creator: ContractAddress } @@ -331,11 +330,6 @@ mod ERC721MultiMetadataComponent { self._set_contract_uri(new_contract_uri); } - fn set_max_supply(ref self: ComponentState<TContractState>, new_max_supply: u64) { - self._assert_only_creator(); - self._set_max_supply(new_max_supply); - } - fn get_base_uri(self: @ComponentState<TContractState>) -> ByteArray { self._base_uri() } @@ -343,10 +337,6 @@ mod ERC721MultiMetadataComponent { fn get_contract_uri(self: @ComponentState<TContractState>) -> ByteArray { self._contract_uri() } - - fn get_max_supply(self: @ComponentState<TContractState>) -> u64 { - self._get_max_supply() - } } // @@ -590,18 +580,6 @@ mod ERC721MultiMetadataComponent { self.ERC721_contract_uri.read() } - fn _set_max_supply(ref self: ComponentState<TContractState>, new_max_supply: u64) { - assert( - U64PartialOrd::lt(new_max_supply, BoundedU64::max()), - 'Cannot Exceed MaxSupply Of U64' - ); - self.ERC721_max_supply.write(new_max_supply); - } - - fn _get_max_supply(self: @ComponentState<TContractState>) -> u64 { - self.ERC721_max_supply.read() - } - fn _assert_only_creator(self: @ComponentState<TContractState>) { let creator: ContractAddress = self.ERC721_creator.read(); let caller: ContractAddress = get_caller_address(); diff --git a/flex_marketplace/src/marketplace/openedition/interfaces/IFlexDropContractMetadata.cairo b/flex_marketplace/src/marketplace/openedition/interfaces/IFlexDropContractMetadata.cairo index 9f93383..6e3b480 100644 --- a/flex_marketplace/src/marketplace/openedition/interfaces/IFlexDropContractMetadata.cairo +++ b/flex_marketplace/src/marketplace/openedition/interfaces/IFlexDropContractMetadata.cairo @@ -4,9 +4,7 @@ use starknet::ContractAddress; trait IFlexDropContractMetadata<TContractState> { fn set_base_uri(ref self: TContractState, new_token_uri: ByteArray); fn set_contract_uri(ref self: TContractState, new_contract_uri: ByteArray); - fn set_max_supply(ref self: TContractState, new_max_supply: u64); fn get_base_uri(self: @TContractState) -> ByteArray; fn get_contract_uri(self: @TContractState) -> ByteArray; - 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 index aceb775..bc795e4 100644 --- a/flex_marketplace/src/marketplace/openedition/interfaces/INonFungibleFlexDropToken.cairo +++ b/flex_marketplace/src/marketplace/openedition/interfaces/INonFungibleFlexDropToken.cairo @@ -31,6 +31,7 @@ trait INonFungibleFlexDropToken<TContractState> { ); 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_mint_state(self: @TContractState, minter: ContractAddress) -> (u64, u64); fn get_current_token_id(self: @TContractState) -> u256; + fn get_allowed_flex_drops(self: @TContractState) -> Span::<ContractAddress>; } diff --git a/flex_marketplace/src/marketplace/utils/openedition.cairo b/flex_marketplace/src/marketplace/utils/openedition.cairo index 11f324c..52cbb5d 100644 --- a/flex_marketplace/src/marketplace/utils/openedition.cairo +++ b/flex_marketplace/src/marketplace/utils/openedition.cairo @@ -13,7 +13,6 @@ struct PhaseDrop { #[derive(Drop, Serde)] struct MultiConfigureStruct { - max_supply: u64, base_uri: ByteArray, contract_uri: ByteArray, flex_drop: ContractAddress, From b614f51bfb05840cc623fbe8746db0b69fb624e7 Mon Sep 17 00:00:00 2001 From: Brian Sin <briansinw3b@gmail.com> Date: Wed, 22 May 2024 18:29:56 +0700 Subject: [PATCH 3/3] feat/whitelist-mint --- .../src/marketplace/marketplace.cairo | 20 +-- .../marketplace/openedition/FlexDrop.cairo | 101 ++++++++++- .../openedition/interfaces/IFlexDrop.cairo | 8 +- .../src/marketplace/signature_checker2.cairo | 167 +++++++++++++++++- .../src/marketplace/utils/openedition.cairo | 7 + .../src/marketplace/utils/order_types.cairo | 4 +- 6 files changed, 285 insertions(+), 22 deletions(-) diff --git a/flex_marketplace/src/marketplace/marketplace.cairo b/flex_marketplace/src/marketplace/marketplace.cairo index 6c50f28..2e67cde 100644 --- a/flex_marketplace/src/marketplace/marketplace.cairo +++ b/flex_marketplace/src/marketplace/marketplace.cairo @@ -350,7 +350,7 @@ mod MarketPlace { self .is_user_order_nonce_executed_or_cancelled - .write((maker_ask.signer, maker_ask.nonce), true); + .write((maker_ask.signer, maker_ask.salt_nonce), true); self .transfer_fees_and_funds( @@ -386,7 +386,7 @@ mod MarketPlace { .emit( TakerBid { order_hash, - order_nonce: maker_ask.nonce, + order_nonce: maker_ask.salt_nonce, taker: non_fungible_token_recipient, maker: maker_ask.signer, strategy: maker_ask.strategy, @@ -434,7 +434,7 @@ mod MarketPlace { self .is_user_order_nonce_executed_or_cancelled - .write((maker_bid.signer, maker_bid.nonce), true); + .write((maker_bid.signer, maker_bid.salt_nonce), true); self .transfer_non_fungible_token( maker_bid.collection, taker_ask.taker, maker_bid.signer, token_id, amount @@ -459,7 +459,7 @@ mod MarketPlace { .emit( TakerAsk { order_hash, - order_nonce: maker_bid.nonce, + order_nonce: maker_bid.salt_nonce, taker: taker_ask.taker, maker: maker_bid.signer, strategy: maker_bid.strategy, @@ -504,10 +504,10 @@ mod MarketPlace { self .is_user_order_nonce_executed_or_cancelled - .write((maker_ask.signer, maker_ask.nonce), true); + .write((maker_ask.signer, maker_ask.salt_nonce), true); self .is_user_order_nonce_executed_or_cancelled - .write((maker_bid.signer, maker_bid.nonce), true); + .write((maker_bid.signer, maker_bid.salt_nonce), true); self .transfer_fees_and_funds( @@ -534,7 +534,7 @@ mod MarketPlace { .emit( TakerBid { order_hash, - order_nonce: maker_ask.nonce, + order_nonce: maker_ask.salt_nonce, taker: maker_bid.signer, maker: maker_ask.signer, strategy: maker_ask.strategy, @@ -715,14 +715,14 @@ mod MarketPlace { self: @ContractState, order: @MakerOrder, order_signature: Array<felt252> ) { let executed_order_cancelled = self - .get_is_user_order_nonce_executed_or_cancelled(*order.signer, *order.nonce); + .get_is_user_order_nonce_executed_or_cancelled(*order.signer, *order.salt_nonce); let min_nonce = self.get_user_min_order_nonce(*order.signer); assert!(!executed_order_cancelled, "MarketPlace: executed order is cancelled"); assert!( - min_nonce <= *order.nonce, + min_nonce <= *order.salt_nonce, "MarketPlace: min_nonce {} is higher than order nonce {}", min_nonce, - *order.nonce + *order.salt_nonce ); assert!( !(*order.signer).is_zero(), "MarketPlace: invalid order signer {}", *order.signer diff --git a/flex_marketplace/src/marketplace/openedition/FlexDrop.cairo b/flex_marketplace/src/marketplace/openedition/FlexDrop.cairo index 30ca8e5..3a4f98c 100644 --- a/flex_marketplace/src/marketplace/openedition/FlexDrop.cairo +++ b/flex_marketplace/src/marketplace/openedition/FlexDrop.cairo @@ -1,14 +1,15 @@ #[starknet::contract] mod FlexDrop { use core::box::BoxTrait; - use flex::marketplace::utils::openedition::PhaseDrop; + use flex::marketplace::utils::openedition::{PhaseDrop, WhiteListParam}; use flex::marketplace::openedition::IFlexDrop; use flex::marketplace::openedition::interfaces::INonFungibleFlexDropToken::{ INonFungibleFlexDropTokenDispatcher, INonFungibleFlexDropTokenDispatcherTrait, I_NON_FUNGIBLE_FLEX_DROP_TOKEN_ID }; use flex::marketplace::{ - currency_manager::{ICurrencyManagerDispatcher, ICurrencyManagerDispatcherTrait} + currency_manager::{ICurrencyManagerDispatcher, ICurrencyManagerDispatcherTrait}, + signature_checker2::{ISignatureChecker2Dispatcher, ISignatureChecker2DispatcherTrait}, }; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use openzeppelin::access::ownable::OwnableComponent; @@ -16,7 +17,7 @@ mod FlexDrop { 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, get_tx_info}; + use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_tx_info, Zeroable}; use array::{Array, ArrayTrait}; component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); @@ -51,6 +52,14 @@ mod FlexDrop { fee_currency: ContractAddress, // start new phase fee new_phase_fee: u256, + // validator validating the proof of whitelist + validator: ContractAddress, + // domain hash + domain_hash: felt252, + // contract to verify proof + signature_checker: ContractAddress, + // mapping proof => is used + is_used_proof: LegacyMap::<felt252, bool>, // mapping nft address => enumerated allowed payer enumerated_allowed_payer: LegacyMap::<ContractAddress, List<ContractAddress>>, currency_manager: ICurrencyManagerDispatcher, @@ -129,12 +138,18 @@ mod FlexDrop { fee_currency: ContractAddress, fee_mint: u256, new_phase_fee: u256, + domain_hash: felt252, + validator: ContractAddress, + signature_checker: ContractAddress, fee_recipients: Span::<ContractAddress> ) { self.ownable.initializer(owner); self.fee_currency.write(fee_currency); self.fee_mint.write(fee_mint); self.new_phase_fee.write(new_phase_fee); + self.validator.write(validator); + self.domain_hash.write(domain_hash); + self.signature_checker.write(signature_checker); self .currency_manager .write(ICurrencyManagerDispatcher { contract_address: currency_manager }); @@ -199,6 +214,51 @@ mod FlexDrop { self.reentrancy.end(); } + fn whitelist_mint( + ref self: ContractState, + whitelist_data: WhiteListParam, + fee_recipient: ContractAddress, + proof: Array<felt252> + ) { + self.pausable.assert_not_paused(); + self.reentrancy.start(); + let phase_drop = self + .phase_drops + .read((whitelist_data.nft_address, whitelist_data.phase_id)); + self.assert_active_phase_drop(@phase_drop); + + let sig_checker_dis = ISignatureChecker2Dispatcher { + contract_address: self.get_signature_checker() + }; + + self.assert_allowed_fee_recipient(@fee_recipient); + + sig_checker_dis + .verify_whitelist_mint_proof( + self.get_domain_hash(), self.get_validator(), whitelist_data, proof + ); + let mint_hash = sig_checker_dis + .compute_whitelist_mint_message_hash( + self.get_domain_hash(), self.get_validator(), whitelist_data + ); + assert(!self.is_used_proof.read(mint_hash), 'FlexDrop: Proof is used'); + + self.is_used_proof.write(mint_hash, true); + + self + .mint_and_pay( + whitelist_data.nft_address, + whitelist_data.minter, + whitelist_data.minter, + false, + 1, + phase_drop.currency, + 0, + fee_recipient + ); + self.reentrancy.end(); + } + fn start_new_phase_drop( ref self: ContractState, phase_drop_id: u64, @@ -371,6 +431,41 @@ mod FlexDrop { self.new_phase_fee.write(new_fee) } + #[external(v0)] + fn update_validator(ref self: ContractState, new_validator: ContractAddress) { + self.ownable.assert_only_owner(); + self.validator.write(new_validator); + } + + #[external(v0)] + fn get_validator(self: @ContractState) -> ContractAddress { + self.validator.read() + } + + #[external(v0)] + fn update_domain_hash(ref self: ContractState, new_domain_hash: felt252) { + self.ownable.assert_only_owner(); + self.domain_hash.write(new_domain_hash); + } + + #[external(v0)] + fn get_domain_hash(self: @ContractState) -> felt252 { + self.domain_hash.read() + } + + #[external(v0)] + fn update_signature_checker( + ref self: ContractState, new_signature_checker: ContractAddress + ) { + self.ownable.assert_only_owner(); + self.signature_checker.write(new_signature_checker); + } + + #[external(v0)] + fn get_signature_checker(self: @ContractState) -> ContractAddress { + self.signature_checker.read() + } + #[external(v0)] fn get_phase_drop( self: @ContractState, nft_address: ContractAddress, phase_id: u64 diff --git a/flex_marketplace/src/marketplace/openedition/interfaces/IFlexDrop.cairo b/flex_marketplace/src/marketplace/openedition/interfaces/IFlexDrop.cairo index 1288fe1..a4088d8 100644 --- a/flex_marketplace/src/marketplace/openedition/interfaces/IFlexDrop.cairo +++ b/flex_marketplace/src/marketplace/openedition/interfaces/IFlexDrop.cairo @@ -1,5 +1,5 @@ use starknet::ContractAddress; -use flex::marketplace::utils::openedition::PhaseDrop; +use flex::marketplace::utils::openedition::{PhaseDrop, WhiteListParam}; #[starknet::interface] trait IFlexDrop<TContractState> { @@ -11,6 +11,12 @@ trait IFlexDrop<TContractState> { minter_if_not_payer: ContractAddress, quantity: u64, ); + fn whitelist_mint( + ref self: TContractState, + whitelist_data: WhiteListParam, + fee_recipient: ContractAddress, + proof: Array<felt252> + ); fn start_new_phase_drop( ref self: TContractState, phase_drop_id: u64, diff --git a/flex_marketplace/src/marketplace/signature_checker2.cairo b/flex_marketplace/src/marketplace/signature_checker2.cairo index a5869ae..2e6b27e 100644 --- a/flex_marketplace/src/marketplace/signature_checker2.cairo +++ b/flex_marketplace/src/marketplace/signature_checker2.cairo @@ -1,35 +1,90 @@ use starknet::ContractAddress; use flex::marketplace::utils::order_types::MakerOrder; +use flex::marketplace::utils::openedition::WhiteListParam; const STARKNET_MESSAGE: felt252 = 110930206544689809660069706067448260453; const HASH_MESSAGE_SELECTOR: felt252 = 563771258078353655219004671487831885088158240957819730493696170021701903504; +const STARKNET_MAKER_ORDER_TYPE_HASH: felt252 = + selector!( + "MakerOrder(is_order_ask:u8,signer:felt,collection:felt,price:u128,seller:felt,token_id:u256,amount:u128,strategy:felt,currency:felt,salt_nonce:u128,start_time:u64,end_time:u64,min_percentage_to_ask:u128,params:felt)u256(low:felt,high:felt)" + ); + +const STARKNET_WHITELIST_TYPE_HASH: felt252 = + selector!("WhiteListParam(phase_id:u64,nft_address:felt,minter:felt)"); + +const U256_TYPE_HASH: felt252 = selector!("u256(low:felt,high:felt)"); + #[starknet::interface] trait ISignatureChecker2<TState> { fn compute_maker_order_hash(self: @TState, hash_domain: felt252, order: MakerOrder) -> felt252; fn verify_maker_order_signature( self: @TState, hash_domain: felt252, order: MakerOrder, order_signature: Array<felt252> ); + fn compute_message_hash(self: @TState, domain_hash: felt252, order: MakerOrder) -> felt252; + fn verify_maker_order_signature_v2( + self: @TState, domain_hash: felt252, order: MakerOrder, order_signature: Array<felt252> + ); + + fn compute_whitelist_mint_message_hash( + self: @TState, domain_hash: felt252, signer: ContractAddress, whitelist_data: WhiteListParam + ) -> felt252; + fn verify_whitelist_mint_proof( + self: @TState, + domain_hash: felt252, + signer: ContractAddress, + whitelist_data: WhiteListParam, + proof: Array<felt252> + ); } #[starknet::contract] mod SignatureChecker2 { + use openzeppelin::account::interface::AccountABIDispatcherTrait; + use core::option::OptionTrait; + use core::traits::Into; + use core::traits::TryInto; + use core::box::BoxTrait; use flex::marketplace::signature_checker2::ISignatureChecker2; - use starknet::ContractAddress; + use starknet::{ContractAddress, get_tx_info, contract_address_to_felt252}; use poseidon::poseidon_hash_span; - + use pedersen::PedersenTrait; + use hash::{HashStateTrait, HashStateExTrait}; + use openzeppelin::account::interface::AccountABIDispatcher; use openzeppelin::account::interface::{ISRC6CamelOnlyDispatcher, ISRC6CamelOnlyDispatcherTrait}; - - use flex::marketplace::utils::order_types::MakerOrder; + use openzeppelin::account::interface::{ISRC6Dispatcher, ISRC6DispatcherTrait}; + use super::{MakerOrder, WhiteListParam}; #[storage] struct Storage {} + #[constructor] + fn constructor(ref self: ContractState) {} + + #[derive(Drop, Copy, Serde, Hash)] + struct StarknetDomain { + name: felt252, + version: felt252, + chain_id: felt252, + } + #[abi(embed_v0)] impl SignatureChecker2Impl of super::ISignatureChecker2<ContractState> { + fn compute_message_hash( + self: @ContractState, domain_hash: felt252, order: MakerOrder + ) -> felt252 { + let mut state = PedersenTrait::new(0); + state = state.update_with('StarkNet Message'); + state = state.update_with(domain_hash); + state = state.update_with(order.signer); + state = state.update_with(order.hash_struct()); + state = state.update_with(4); + state.finalize() + } + fn compute_maker_order_hash( self: @ContractState, hash_domain: felt252, order: MakerOrder ) -> felt252 { @@ -43,7 +98,7 @@ mod SignatureChecker2 { order.amount.into(), order.strategy.into(), order.currency.into(), - order.nonce.into(), + order.salt_nonce.into(), order.start_time.into(), order.end_time.into(), order.min_percentage_to_ask.into(), @@ -65,10 +120,108 @@ mod SignatureChecker2 { order_signature: Array<felt252> ) { let hash = self.compute_maker_order_hash(hash_domain, order); - let result = ISRC6CamelOnlyDispatcher { contract_address: order.signer } - .isValidSignature(hash, order_signature); + let result = ISRC6Dispatcher { contract_address: order.signer } + .is_valid_signature(hash, order_signature); assert!(result == starknet::VALIDATED, "SignatureChecker: Invalid signature"); } + + fn verify_maker_order_signature_v2( + self: @ContractState, + domain_hash: felt252, + order: MakerOrder, + order_signature: Array<felt252> + ) { + let hash = self.compute_message_hash(domain_hash, order); + let account: AccountABIDispatcher = AccountABIDispatcher { + contract_address: order.signer + }; + let result = account.is_valid_signature(hash, order_signature); + + assert!(result == starknet::VALIDATED, "SignatureChecker: Invalid signature"); + } + + fn compute_whitelist_mint_message_hash( + self: @ContractState, + domain_hash: felt252, + signer: ContractAddress, + whitelist_data: WhiteListParam + ) -> felt252 { + let mut state = PedersenTrait::new(0); + state = state.update_with('StarkNet Message'); + state = state.update_with(domain_hash); + state = state.update_with(signer); + state = state.update_with(whitelist_data.hash_struct()); + state = state.update_with(4); + state.finalize() + } + + fn verify_whitelist_mint_proof( + self: @ContractState, + domain_hash: felt252, + signer: ContractAddress, + whitelist_data: WhiteListParam, + proof: Array<felt252> + ) { + let hash = self + .compute_whitelist_mint_message_hash(domain_hash, signer, whitelist_data); + let account: AccountABIDispatcher = AccountABIDispatcher { contract_address: signer }; + let result = account.is_valid_signature(hash, proof); + + assert!(result == starknet::VALIDATED, "SignatureChecker: Invalid proof"); + } + } + + trait IStructHash<T> { + fn hash_struct(self: @T) -> felt252; + } + + impl StructHashWhiteList of IStructHash<WhiteListParam> { + fn hash_struct(self: @WhiteListParam) -> felt252 { + let mut state = PedersenTrait::new(0); + state = state.update_with(super::STARKNET_WHITELIST_TYPE_HASH); + state = state.update_with(*self.phase_id); + state = state.update_with(contract_address_to_felt252(*self.nft_address)); + state = state.update_with(contract_address_to_felt252(*self.minter)); + state = state.update_with(4); + state.finalize() + } + } + + impl StructHashMarkerOrder of IStructHash<MakerOrder> { + fn hash_struct(self: @MakerOrder) -> felt252 { + let mut state = PedersenTrait::new(0); + state = state.update_with(super::STARKNET_MAKER_ORDER_TYPE_HASH); + let mut is_order_ask_u8: u8 = 1; + if !(*self.is_order_ask) { + is_order_ask_u8 = 0; + } + state = state.update_with(is_order_ask_u8); + state = state.update_with(contract_address_to_felt252(*self.signer)); + state = state.update_with(contract_address_to_felt252(*self.collection)); + state = state.update_with(*self.price); + state = state.update_with(contract_address_to_felt252(*self.seller)); + state = state.update_with(self.token_id.hash_struct()); + state = state.update_with(*self.amount); + state = state.update_with(contract_address_to_felt252(*self.strategy)); + state = state.update_with(contract_address_to_felt252(*self.currency)); + state = state.update_with(*self.salt_nonce); + state = state.update_with(*self.start_time); + state = state.update_with(*self.end_time); + state = state.update_with(*self.min_percentage_to_ask); + state = state.update_with(*self.params); + state = state.update_with(15); + state.finalize() + } + } + + impl StructHashU256 of IStructHash<u256> { + fn hash_struct(self: @u256) -> felt252 { + let mut state = PedersenTrait::new(0); + state = state.update_with(super::U256_TYPE_HASH); + state = state.update_with(*self); + state = state.update_with(3); + state.finalize() + } } } diff --git a/flex_marketplace/src/marketplace/utils/openedition.cairo b/flex_marketplace/src/marketplace/utils/openedition.cairo index 52cbb5d..24c3674 100644 --- a/flex_marketplace/src/marketplace/utils/openedition.cairo +++ b/flex_marketplace/src/marketplace/utils/openedition.cairo @@ -11,6 +11,13 @@ struct PhaseDrop { phase_type: u8 // 1 for public sale, 2 for private sale... } +#[derive(Drop, Copy, Serde)] +struct WhiteListParam { + phase_id: u64, + nft_address: ContractAddress, + minter: ContractAddress, +} + #[derive(Drop, Serde)] struct MultiConfigureStruct { base_uri: ByteArray, diff --git a/flex_marketplace/src/marketplace/utils/order_types.cairo b/flex_marketplace/src/marketplace/utils/order_types.cairo index 6ed85f2..817a9a4 100644 --- a/flex_marketplace/src/marketplace/utils/order_types.cairo +++ b/flex_marketplace/src/marketplace/utils/order_types.cairo @@ -7,11 +7,12 @@ struct MakerOrder { signer: ContractAddress, // signer of the maker order collection: ContractAddress, // collection address price: u128, + seller: ContractAddress, token_id: u256, amount: u128, // amount of tokens to sell/purchase (must be 1 for ERC721, 1+ for ERC1155) strategy: ContractAddress, // strategy address for trade execution (e.g. StandardSaleForFixedPrice) currency: ContractAddress, // currency address - nonce: u128, // order nonce (must be unique unless new maker order is meant to override existing one e.g. lower ask price) + salt_nonce: u128, // order nonce (must be unique unless new maker order is meant to override existing one e.g. lower ask price) start_time: u64, // startTime in timestamp end_time: u64, // endTime in timestamp min_percentage_to_ask: u128, // slippage protection (9000 = 90% of the final price must return to ask) @@ -24,6 +25,7 @@ struct TakerOrder { taker: ContractAddress, // caller price: u128, // final price for the purchase token_id: u256, + amount: u128, min_percentage_to_ask: u128, // slippage protection (9000 = 90% of the final price must return to ask) params: felt252, }