diff --git a/contracts/ark_common/src/protocol/order_types.cairo b/contracts/ark_common/src/protocol/order_types.cairo index f2ffb7ee..2f5367b2 100644 --- a/contracts/ark_common/src/protocol/order_types.cairo +++ b/contracts/ark_common/src/protocol/order_types.cairo @@ -11,6 +11,8 @@ enum OrderType { Auction, Offer, CollectionOffer, + LimitBuy, + LimitSell } impl OrderTypeIntoFelt252 of Into { @@ -20,6 +22,8 @@ impl OrderTypeIntoFelt252 of Into { OrderType::Auction => 'AUCTION', OrderType::Offer => 'OFFER', OrderType::CollectionOffer => 'COLLECTION_OFFER', + OrderType::LimitBuy => 'LIMIT_BUY', + OrderType::LimitSell => 'LIMIT_SELL' } } } @@ -34,6 +38,10 @@ impl Felt252TryIntoOrderType of TryInto { Option::Some(OrderType::Offer) } else if self == 'COLLECTION_OFFER' { Option::Some(OrderType::CollectionOffer) + } else if self == 'LIMIT_BUY' { + Option::Some(OrderType::LimitBuy) + } else if self == 'LIMIT_SELL' { + Option::Some(OrderType::LimitSell) } else { Option::None } @@ -221,8 +229,8 @@ struct ExecutionInfo { token_address: ContractAddress, token_from: ContractAddress, token_to: ContractAddress, - token_id: u256, token_quantity: u256, + token_id: OptionU256, payment_from: ContractAddress, payment_to: ContractAddress, payment_amount: u256, @@ -265,6 +273,8 @@ enum RouteType { #[default] Erc20ToErc721, Erc721ToErc20, + Erc20ToErc20Buy, + Erc20ToErc20Sell, Erc20ToErc1155, Erc1155ToErc20, } @@ -274,6 +284,8 @@ impl RouteIntoFelt252 of Into { match self { RouteType::Erc20ToErc721 => 'ERC20TOERC721', RouteType::Erc721ToErc20 => 'ERC721TOERC20', + RouteType::Erc20ToErc20Buy => 'ERC20TOERC20BUY', + RouteType::Erc20ToErc20Sell => 'ERC20TOERC20SELL', RouteType::Erc20ToErc1155 => 'ERC20TOERC1155', RouteType::Erc1155ToErc20 => 'ERC1155TOERC20', } @@ -286,6 +298,10 @@ impl Felt252TryIntoRoute of TryInto { Option::Some(RouteType::Erc20ToErc721) } else if self == 'ERC721TOERC20' { Option::Some(RouteType::Erc721ToErc20) + } else if self == 'ERC20TOERC20BUY' { + Option::Some(RouteType::Erc20ToErc20Buy) + } else if self == 'ERC20TOERC20SELL' { + Option::Some(RouteType::Erc20ToErc20Sell) } else if self == 'ERC1155TOERC20' { Option::Some(RouteType::Erc1155ToErc20) } else if self == 'ERC20TOERC1155' { @@ -295,3 +311,27 @@ impl Felt252TryIntoRoute of TryInto { } } } + +#[derive(starknet::Store, Serde, Drop, PartialEq, Copy, Debug)] +struct OptionU256 { + is_some: felt252, + value: u256, +} + +trait OptionU256Trait, +Drop> { + fn get_some(self: @T) -> (felt252, u256); + fn is_some(self: @T) -> bool; +} + +impl OptionU256Impl of OptionU256Trait { + fn get_some(self: @OptionU256) -> (felt252, u256) { + (*self.is_some, *self.value) + } + fn is_some(self: @OptionU256) -> bool { + if *self.is_some == 1 { + true + } else { + false + } + } +} diff --git a/contracts/ark_common/src/protocol/order_v1.cairo b/contracts/ark_common/src/protocol/order_v1.cairo index b98b3947..b22a27b6 100644 --- a/contracts/ark_common/src/protocol/order_v1.cairo +++ b/contracts/ark_common/src/protocol/order_v1.cairo @@ -19,10 +19,9 @@ const ORDER_VERSION_V1: felt252 = 'v1'; #[derive(Serde, Drop, Copy)] struct OrderV1 { - // Route ERC20->ERC721 (buy) ERC721->ERC20 (sell) + // Route ERC20->ERC721 (buy) ERC721->ERC20 (sell) ERC20BUY ERC20SELL route: RouteType, - // Contract address of the currency used on Starknet for the transfer. - // For now ERC20 -> ETH Starkgate. + // Contract address of the payment currency used on Starknet for the transfer. currency_address: ContractAddress, currency_chain_id: felt252, // Salt. @@ -31,16 +30,16 @@ struct OrderV1 { offerer: ContractAddress, // Chain id. token_chain_id: felt252, - // The token contract address. + // The token contract address. // exchange token token_address: ContractAddress, // The token id. token_id: Option, // The quantity of the token_id to be offerred (1 for NFTs). quantity: u256, // in wei. --> 10 | 10 | 10 | - start_amount: u256, + start_amount: u256, // amount to pay for buy order. // in wei. --> 0 | 10 | 20 | - end_amount: u256, + end_amount: u256, // amount to receive for sell order // Start validity date of the offer, seconds since unix epoch. start_date: u64, // Expiry date of the order, seconds since unix epoch. @@ -64,16 +63,31 @@ impl OrderTraitOrderV1 of OrderTrait { return Result::Err(OrderValidationError::InvalidSalt); } - let end_date = *self.end_date; + // check for expiry only if not erc20 buys or sells + if (*self.route != RouteType::Erc20ToErc20Buy + && *self.route != RouteType::Erc20ToErc20Sell) { + let end_date = *self.end_date; - if end_date < block_timestamp { - return Result::Err(OrderValidationError::EndDateInThePast); + if end_date < block_timestamp { + return Result::Err(OrderValidationError::EndDateInThePast); + } + + // End date -> block_timestamp + 30 days. + let max_end_date = block_timestamp + (30 * 24 * 60 * 60); + if end_date > max_end_date { + return Result::Err(OrderValidationError::EndDateTooFar); + } } - // End date -> block_timestamp + 30 days. - let max_end_date = block_timestamp + (30 * 24 * 60 * 60); - if end_date > max_end_date { - return Result::Err(OrderValidationError::EndDateTooFar); + // check that the start amount is not zero for sell erc20 orders + if (*self.route != RouteType::Erc20ToErc20Sell) { + if (*self.start_amount).is_zero() { + return Result::Err(OrderValidationError::InvalidContent); + } + } else { + if (*self.end_amount).is_zero() { + return Result::Err(OrderValidationError::InvalidContent); + } } // TODO: define a real value here. 20 is an example and @@ -86,7 +100,6 @@ impl OrderTraitOrderV1 of OrderTrait { if (*self.offerer).is_zero() || (*self.token_address).is_zero() - || (*self.start_amount).is_zero() || (*self.salt).is_zero() || (*self.quantity).is_zero() { return Result::Err(OrderValidationError::InvalidContent); @@ -126,6 +139,26 @@ impl OrderTraitOrderV1 of OrderTrait { || (*self.route) == RouteType::Erc20ToErc1155) { return Result::Ok(OrderType::CollectionOffer); } + + // Limit Buy Order + if (*self.quantity) > 0 + && (*self.start_amount) > 0 // amount to pay + && (*self.end_amount).is_zero() + && (*self.route == RouteType::Erc20ToErc20Buy) { + return Result::Ok(OrderType::LimitBuy); + } + + // Limit Sell Order + if (*self.quantity) > 0 + && (*self.start_amount).is_zero() + && (*self.end_amount) > 0 // amount to receive + && (*self.route == RouteType::Erc20ToErc20Sell) { + return Result::Ok(OrderType::LimitSell); + } + + if (*self.route == RouteType::Erc20ToErc20Sell) { + return Result::Ok(OrderType::LimitSell); + } } // Other order types are not supported. @@ -133,14 +166,23 @@ impl OrderTraitOrderV1 of OrderTrait { } fn compute_token_hash(self: @OrderV1) -> felt252 { - assert(OptionTrait::is_some(self.token_id), 'Token ID expected'); - let token_id = (*self.token_id).unwrap(); - let mut buf: Array = array![]; - buf.append((token_id.low).into()); - buf.append((token_id.high).into()); - buf.append((*self.token_address).into()); - buf.append(*self.token_chain_id); - poseidon_hash_span(buf.span()) + if (*self.route == RouteType::Erc20ToErc20Buy + || *self.route == RouteType::Erc20ToErc20Sell) { + let mut buf: Array = array![]; + // used quantity, start_date and the offerer as the identifiers + buf.append((*self.token_address).into()); + buf.append(*self.token_chain_id); + poseidon_hash_span(buf.span()) + } else { + assert(OptionTrait::is_some(self.token_id), 'Token ID expected'); + let token_id = (*self.token_id).unwrap(); + let mut buf: Array = array![]; + buf.append((token_id.low).into()); + buf.append((token_id.high).into()); + buf.append((*self.token_address).into()); + buf.append(*self.token_chain_id); + poseidon_hash_span(buf.span()) + } } fn compute_order_hash(self: @OrderV1) -> felt252 { diff --git a/contracts/ark_component/src/orderbook/interface.cairo b/contracts/ark_component/src/orderbook/interface.cairo index b8d6fa50..492545ef 100644 --- a/contracts/ark_component/src/orderbook/interface.cairo +++ b/contracts/ark_component/src/orderbook/interface.cairo @@ -31,8 +31,13 @@ pub mod orderbook_errors { const ORDER_TOKEN_ID_IS_MISSING: felt252 = 'OB: token id is missing'; const ORDER_TOKEN_HASH_DOES_NOT_MATCH: felt252 = 'OB: token hash does not match'; const ORDER_NOT_AN_OFFER: felt252 = 'OB: order is not an offer'; + const ORDER_NOT_AN_ERC20_ORDER: felt252 = 'OB: order not erc 20'; + const ORDER_ROUTE_NOT_ERC20: felt252 = 'OB: order route not erc 20'; + const ORDER_ROUTE_NOT_VALID: felt252 = 'OB: order not valid erc 20'; + const ORDER_PRICE_NOT_MATCH: felt252 = 'OB: order price not match'; const ORDER_NOT_OPEN: felt252 = 'OB: order is not open'; const ORDER_OPEN: felt252 = 'OB: order is not open'; + const ORDER_NOT_SUPPORTED: felt252 = 'OB: order not supported'; const USE_FULFILL_AUCTION: felt252 = 'OB: must use fulfill auction'; const OFFER_NOT_STARTED: felt252 = 'OB: offer is not started'; const INVALID_BROKER: felt252 = 'OB: broker is not whitelisted'; diff --git a/contracts/ark_component/src/orderbook/orderbook.cairo b/contracts/ark_component/src/orderbook/orderbook.cairo index 0ed79981..01a4044a 100644 --- a/contracts/ark_component/src/orderbook/orderbook.cairo +++ b/contracts/ark_component/src/orderbook/orderbook.cairo @@ -5,7 +5,7 @@ pub mod OrderbookComponent { }; use ark_common::protocol::order_types::{ OrderStatus, OrderTrait, OrderType, CancelInfo, FulfillInfo, ExecutionValidationInfo, - ExecutionInfo, RouteType + ExecutionInfo, RouteType, OptionU256 }; use ark_common::protocol::order_v1::OrderV1; use core::debug::PrintTrait; @@ -17,6 +17,7 @@ pub mod OrderbookComponent { use core::zeroable::Zeroable; use starknet::ContractAddress; use starknet::storage::Map; + use super::super::interface::{IOrderbook, IOrderbookAction, orderbook_errors}; const EXTENSION_TIME_IN_SECONDS: u64 = 600; @@ -33,6 +34,10 @@ pub mod OrderbookComponent { auctions: Map, /// Mapping of auction offer order_hash to auction listing order_hash. auction_offers: Map, + /// Mapping of erc20s buy orderhash to the order (price, quantity) + buy_orders: Map, + /// Mapping of erc20s sell orderhash to the order (price, quantity) + sell_orders: Map } // ************************************************************************* @@ -50,6 +55,9 @@ pub mod OrderbookComponent { OrderFulfilled: OrderFulfilled, } + // precision for erc20 price division + const PRECISION: u256 = 1000000000000000000; + // must be increased when `OrderPlaced` content change pub const ORDER_PLACED_EVENT_VERSION: u8 = 1; /// Event for when an order is placed. @@ -275,6 +283,8 @@ pub mod OrderbookComponent { OrderType::CollectionOffer => { self._create_collection_offer(order, order_type, order_hash); }, + OrderType::LimitBuy => { self._create_limit_order(order, order_type, order_hash); }, + OrderType::LimitSell => { self._create_limit_order(order, order_type, order_hash); } }; HooksCreateOrder::after_create_order(ref self, order); @@ -282,14 +292,15 @@ pub mod OrderbookComponent { fn cancel_order(ref self: ComponentState, cancel_info: CancelInfo) { HooksCancelOrder::before_cancel_order(ref self, cancel_info); - let order_hash = cancel_info.order_hash; let order_option = order_read::(order_hash); assert(order_option.is_some(), orderbook_errors::ORDER_NOT_FOUND); let order = order_option.unwrap(); assert(order.offerer == cancel_info.canceller, 'not the same offerrer'); match order_status_read(order_hash) { - Option::Some(s) => s, + Option::Some(s) => assert( + s == OrderStatus::Open, orderbook_errors::ORDER_FULFILLED + ), Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), }; let block_ts = starknet::get_block_timestamp(); @@ -302,15 +313,21 @@ pub mod OrderbookComponent { block_ts <= auction_end_date, orderbook_errors::ORDER_AUCTION_IS_EXPIRED ); self.auctions.write(auction_token_hash, (0, 0, 0)); + } else if order_type == OrderType::LimitBuy { + self.buy_orders.write(order_hash, (0, 0)); + } else if order_type == OrderType::LimitSell { + self.sell_orders.write(order_hash, (0, 0)); } else { assert(block_ts < order.end_date, orderbook_errors::ORDER_IS_EXPIRED); if order_type == OrderType::Listing { - self.token_listings.write(order.compute_token_hash(), 0); + let order_hash = order.compute_token_hash(); + self.token_listings.write(order_hash, 0); } - } + }; + order_type }, - Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), + Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND) }; // Cancel order @@ -352,6 +369,8 @@ pub mod OrderbookComponent { OrderType::Auction => self._fulfill_auction_order(fulfill_info, order), OrderType::Offer => self._fulfill_offer(fulfill_info, order), OrderType::CollectionOffer => self._fulfill_offer(fulfill_info, order), + OrderType::LimitBuy => self._fulfill_limit_order(fulfill_info, order), + OrderType::LimitSell => self._fulfill_limit_order(fulfill_info, order), }; self @@ -453,7 +472,7 @@ pub mod OrderbookComponent { token_address: order.token_address, token_from: order.offerer, token_to: related_order.offerer, - token_id: order.token_id.unwrap(), + token_id: OptionU256 { is_some: 1, value: order.token_id.unwrap() }, token_quantity: order.quantity, payment_from: related_order.offerer, payment_to: fulfill_info.fulfiller, @@ -500,7 +519,7 @@ pub mod OrderbookComponent { token_address: order.token_address, token_from: fulfill_info.fulfiller, token_to: order.offerer, - token_id: fulfill_info.token_id.unwrap(), + token_id: OptionU256 { is_some: 1, value: fulfill_info.token_id.unwrap() }, token_quantity: order.quantity, payment_from: order.offerer, payment_to: fulfill_info.fulfiller, @@ -534,7 +553,7 @@ pub mod OrderbookComponent { token_address: order.token_address, token_from: order.offerer, token_to: fulfill_info.fulfiller, - token_id: order.token_id.unwrap(), + token_id: OptionU256 { is_some: 1, value: order.token_id.unwrap() }, token_quantity: order.quantity, payment_from: fulfill_info.fulfiller, payment_to: order.offerer, @@ -807,6 +826,213 @@ pub mod OrderbookComponent { } ); } + + /// Creates a limit buy order + fn _create_limit_order( + ref self: ComponentState, + order: OrderV1, + order_type: OrderType, + order_hash: felt252 + ) { + // revert if order is fulfilled or Open + let (price, _) = self.buy_orders.read(order_hash); + if (price.is_non_zero()) { + assert( + order_status_read(order_hash) != Option::Some(OrderStatus::Fulfilled), + panic_with_felt252(orderbook_errors::ORDER_FULFILLED) + ); + } + let cancelled_order_hash = self._process_previous_order(order_hash, order.offerer); + + order_write(order_hash, order_type, order); + + match order_type { + OrderType::LimitBuy => { + let price = order.start_amount / order.quantity * PRECISION; + self.buy_orders.write(order_hash, (price, order.quantity)); + }, + OrderType::LimitSell => { + let price = order.end_amount / order.quantity * PRECISION; + self.sell_orders.write(order_hash, (price, order.quantity)); + }, + _ => () + } + + self + .emit( + OrderPlaced { + order_hash: order_hash, + order_version: order.get_version(), + order_type: order_type, + version: ORDER_PLACED_EVENT_VERSION, + cancelled_order_hash, + order: order + } + ); + } + + fn _create_listing_execution_info( + ref self: ComponentState, + order_hash: felt252, + buy_order: OrderV1, + sell_order: OrderV1, + fulfill_info: FulfillInfo, + token_quantity: u256, + listing_broker_address: ContractAddress, + price: u256 + ) -> ExecutionInfo { + ExecutionInfo { + order_hash, + token_address: buy_order.token_address, + token_from: sell_order.offerer, + token_to: buy_order.offerer, + token_id: OptionU256 { is_some: 0, value: 0 }, + token_quantity, + payment_from: buy_order.offerer, + payment_to: sell_order.offerer, + payment_amount: price * token_quantity / PRECISION, + payment_currency_address: buy_order.currency_address, + payment_currency_chain_id: buy_order.currency_chain_id, + listing_broker_address: listing_broker_address, + fulfill_broker_address: fulfill_info.fulfill_broker_address, + } + } + + /// Fulfill limit order + fn _fulfill_limit_order( + ref self: ComponentState, fulfill_info: FulfillInfo, order: OrderV1 + ) -> (Option, Option) { + let order_hash = order.compute_order_hash(); + + assert( + order_hash == fulfill_info.order_hash, orderbook_errors::ORDER_HASH_DOES_NOT_MATCH + ); + + let related_order_hash = fulfill_info + .related_order_hash + .expect(orderbook_errors::ORDER_MISSING_RELATED_ORDER); + + match order_type_read(related_order_hash) { + Option::Some(order_type) => { + assert( + order_type == OrderType::LimitBuy || order_type == OrderType::LimitSell, + orderbook_errors::ORDER_NOT_AN_ERC20_ORDER + ); + }, + Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), + } + + match order_status_read(related_order_hash) { + Option::Some(s) => { + assert(s == OrderStatus::Open, orderbook_errors::ORDER_NOT_OPEN); + s + }, + Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), + }; + + let related_order = match order_read::(related_order_hash) { + Option::Some(o) => o, + Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), + }; + + let related_order_token_hash = related_order.compute_token_hash(); + + // check that they are both the same token + assert( + related_order_token_hash == order.compute_token_hash(), + orderbook_errors::ORDER_TOKEN_HASH_DOES_NOT_MATCH + ); + + let (buy_order, sell_order) = match order.route { + RouteType::Erc20ToErc20Sell => { + assert( + related_order.route == RouteType::Erc20ToErc20Buy, + orderbook_errors::ORDER_ROUTE_NOT_VALID + ); + (related_order, order) + }, + RouteType::Erc20ToErc20Buy => { + assert( + related_order.route == RouteType::Erc20ToErc20Sell, + orderbook_errors::ORDER_ROUTE_NOT_VALID + ); + (order, related_order) + }, + _ => panic!("route not supported") + }; + + // add 1e18 to the multiplication; + + // check that the price is the same + let buy_price = buy_order.start_amount / buy_order.quantity * PRECISION; + let sell_price = sell_order.end_amount / sell_order.quantity * PRECISION; + + let buy_order_hash = buy_order.compute_order_hash(); + let sell_order_hash = sell_order.compute_order_hash(); + + assert(buy_price == sell_price, orderbook_errors::ORDER_PRICE_NOT_MATCH); + + let (_, buy_order_quantity) = self.buy_orders.read(buy_order_hash); + let (_, sell_order_quantity) = self.sell_orders.read(sell_order_hash); + + if buy_order_quantity > sell_order_quantity { + // reduce buy quantity order and execute sell order + self + .buy_orders + .write(buy_order_hash, (buy_price, buy_order_quantity - sell_order_quantity)); + // set buy order as fufilled + order_status_write(sell_order_hash, OrderStatus::Fulfilled); + // set execute info + let execute_info = self + ._create_listing_execution_info( + sell_order_hash, + buy_order, + sell_order, + fulfill_info, + sell_order_quantity, + related_order.broker_id, + buy_price + ); + (Option::Some(execute_info), Option::Some(related_order_hash)) + } else if sell_order_quantity > buy_order_quantity { + // reduce sell quantity, and execute buy order + self + .sell_orders + .write(sell_order_hash, (sell_price, sell_order_quantity - buy_order_quantity)); + // set sell order as fulfilled + order_status_write(buy_order_hash, OrderStatus::Fulfilled); + // generate execution info + let execute_info = self + ._create_listing_execution_info( + buy_order_hash, + buy_order, + sell_order, + fulfill_info, + buy_order_quantity, + order.broker_id, + buy_price + ); + (Option::Some(execute_info), Option::Some(related_order_hash)) + } else { + // execute both orders + order_status_write(buy_order_hash, OrderStatus::Fulfilled); + order_status_write(sell_order_hash, OrderStatus::Fulfilled); + // passing any of them as the order hash will fulfill both orders, + // so just one executioninfo will be sent. + let execute_info = self + ._create_listing_execution_info( + buy_order_hash, + buy_order, + sell_order, + fulfill_info, + buy_order_quantity, + order.broker_id, + buy_price + ); + // return + (Option::Some(execute_info), Option::Some(related_order_hash)) + } + } } } pub impl OrderbookHooksCreateOrderEmptyImpl< diff --git a/contracts/ark_starknet/Scarb.toml b/contracts/ark_starknet/Scarb.toml index b1b5436c..6265f910 100644 --- a/contracts/ark_starknet/Scarb.toml +++ b/contracts/ark_starknet/Scarb.toml @@ -30,6 +30,7 @@ casm = true allowed-libfuncs-list.name = "experimental" build-external-contracts = [ "ark_tokens::erc20::FreeMintERC20", + "ark_tokens::erc20_trade::TradeERC20", "ark_tokens::erc721::FreeMintNFT", "ark_tokens::erc721_royalty::FreeMintNFTRoyalty", "ark_tokens::erc1155::FreeMintERC1155", diff --git a/contracts/ark_starknet/src/executor.cairo b/contracts/ark_starknet/src/executor.cairo index e9572e7b..a4810637 100644 --- a/contracts/ark_starknet/src/executor.cairo +++ b/contracts/ark_starknet/src/executor.cairo @@ -1,10 +1,8 @@ use ark_common::protocol::order_types::OrderTrait; -use ark_common::protocol::order_types::OrderType; - +use ark_common::protocol::order_types::{OrderType, RouteType}; use starknet::ContractAddress; - #[derive(Drop, Copy, Debug)] struct OrderInfo { order_type: OrderType, @@ -13,14 +11,15 @@ struct OrderInfo { // The token contract address. token_address: ContractAddress, // The token id. - // TODO: how to store Option ? token_id: u256, - // The quantity of the token (1 for ERC-721, variable for ERC-1155). - quantity: u256, // in wei. --> 10 | 10 | 10 | - start_amount: u256, - // + start_amount: u256, // amount to pay + // address making the order offerer: ContractAddress, + // number of tokens + quantity: u256, // 0 for ERC721, + // route type + route: RouteType } //! Executor contract on Starknet @@ -35,7 +34,7 @@ struct OrderInfo { mod executor { use ark_common::protocol::order_types::{ RouteType, ExecutionInfo, ExecutionValidationInfo, FulfillInfo, CreateOrderInfo, - FulfillOrderInfo, CancelOrderInfo, CancelInfo, OrderType, + FulfillOrderInfo, CancelOrderInfo, CancelInfo, OrderType, OptionU256, OptionU256Trait }; use ark_common::protocol::order_v1::{OrderV1, OrderTraitOrderV1}; @@ -185,9 +184,9 @@ mod executor { } fn get_collection_creator_fees( - self: @ContractState, nft_address: ContractAddress + self: @ContractState, token_address: ContractAddress ) -> (ContractAddress, FeesRatio) { - let (receiver, fees_ratio) = self.creator_fees.read(nft_address); + let (receiver, fees_ratio) = self.creator_fees.read(token_address); if fees_ratio.denominator.is_zero() { self.get_default_creator_fees() } else { @@ -197,21 +196,21 @@ mod executor { fn set_collection_creator_fees( ref self: ContractState, - nft_address: ContractAddress, + token_address: ContractAddress, receiver: ContractAddress, fees_ratio: FeesRatio ) { _ensure_admin(@self); assert(fees_ratio.is_valid(), Errors::FEES_RATIO_INVALID); - self.creator_fees.write(nft_address, (receiver, fees_ratio)); + self.creator_fees.write(token_address, (receiver, fees_ratio)); } fn get_fees_amount( self: @ContractState, fulfill_broker: ContractAddress, listing_broker: ContractAddress, - nft_address: ContractAddress, - nft_token_id: u256, + token_address: ContractAddress, + token_id: OptionU256, payment_amount: u256 ) -> FeesAmount { let ( @@ -221,7 +220,7 @@ mod executor { creator_fees_amount ) = _compute_fees_amount( - self, fulfill_broker, listing_broker, nft_address, nft_token_id, payment_amount + self, fulfill_broker, listing_broker, token_address, token_id, payment_amount ); FeesAmount { @@ -327,6 +326,20 @@ mod executor { Option::None => panic!("Invalid token id"), } }, + RouteType::Erc20ToErc20Buy => { + assert!( + _check_erc20_amount( + order.currency_address, *(order.start_amount), order.offerer + ), + "Oferrer does not own enough ERC20 tokens to buy" + ) + }, + RouteType::Erc20ToErc20Sell => { + assert!( + _check_erc20_amount(order.token_address, *(order.quantity), order.offerer), + "Oferrer does not own enough ERC20 tokens to sell" + ) + }, RouteType::Erc1155ToErc20 => { match order.token_id { Option::Some(token_id) => { @@ -371,26 +384,32 @@ mod executor { panic!("Order not found"); } - if order_info.order_type != OrderType::Auction { - assert!(order_info.offerer != fulfiller, "Offerer and fulfiller must be different"); - } - let contract_address = get_contract_address(); match order_info.order_type { OrderType::Listing => { + assert!(order_info.offerer != fulfiller, "Offerer and fulfiller must be different"); _verify_fulfill_listing_order(self, order_info, fulfill_info, contract_address); }, OrderType::Offer => { + assert!(order_info.offerer != fulfiller, "Offerer and fulfiller must be different"); _verify_fulfill_offer_order(self, order_info, fulfill_info, contract_address); }, OrderType::Auction => { _verify_fulfill_auction_order(self, order_info, fulfill_info, contract_address); }, OrderType::CollectionOffer => { + assert!(order_info.offerer != fulfiller, "Offerer and fulfiller must be different"); _verify_fulfill_collection_offer_order( self, order_info, fulfill_info, contract_address ); - } + }, + OrderType::LimitBuy => { + _verify_limit_order(self, order_info, fulfill_info, contract_address) + }, + OrderType::LimitSell => { + _verify_limit_order(self, order_info, fulfill_info, contract_address) + }, + _ => panic!("Order not supported") } } @@ -577,6 +596,74 @@ mod executor { } } + + fn _verify_limit_order( + self: @ContractState, + order_info: OrderInfo, + fulfill_info: @FulfillInfo, + contract_address: ContractAddress + ) { + let related_order_info = match *(fulfill_info.related_order_hash) { + Option::None => panic!("Fulfill limit order require a related order"), + Option::Some(related_order_hash) => _get_order_info(self, related_order_hash), + }; + assert!( + @order_info.currency_address == @related_order_info.currency_address, + "Order and related order use different currency" + ); + + let (buyer_order, seller_order) = match order_info.route { + RouteType::Erc20ToErc20Sell => { + assert( + related_order_info.route == RouteType::Erc20ToErc20Buy, 'Order route not valid' + ); + + (related_order_info, order_info) + }, + RouteType::Erc20ToErc20Buy => { + assert( + related_order_info.route == RouteType::Erc20ToErc20Sell, 'Order route not valid' + ); + (order_info, related_order_info) + }, + _ => panic!("route not supported") + }; + + let buyer = buyer_order.offerer; + + //checks for buyer + assert!( + _check_erc20_amount(@buyer_order.currency_address, buyer_order.start_amount, @buyer), + "Buyer does not own enough ERC20 tokens" + ); + + assert!( + _check_erc20_allowance( + @buyer_order.currency_address, + buyer_order.start_amount, + @buyer, + @get_contract_address() + ), + "Buyer's allowance of executor is not enough" + ); + + let seller = seller_order.offerer; + + // checks for seller + assert!( + _check_erc20_amount(@seller_order.token_address, seller_order.quantity, @seller), + "Seller does not own enough ERC20 tokens" + ); + + assert!( + _check_erc20_allowance( + @seller_order.token_address, seller_order.quantity, @seller, @get_contract_address() + ), + "Seller's allowance of executor is not enough" + ); + } + + fn _verify_fulfill_collection_offer_order( self: @ContractState, order_info: OrderInfo, @@ -648,7 +735,7 @@ mod executor { @self, @execution_info.token_address, execution_info.payment_amount, - execution_info.token_id + execution_info.token_id, ); let (fulfill_broker_fees_amount, listing_broker_fees_amount, ark_fees_amount, _) = _compute_fees_amount( @@ -659,6 +746,7 @@ mod executor { execution_info.token_id, execution_info.payment_amount ); + assert!( execution_info .payment_amount > (fulfill_broker_fees_amount @@ -724,38 +812,64 @@ mod executor { ); } - if _is_erc721(execution_info.token_address) { - let nft_contract = IERC721Dispatcher { contract_address: execution_info.token_address }; - nft_contract - .transfer_from( - execution_info.token_from, execution_info.token_to, execution_info.token_id - ); - } + let (is_some, token_id) = execution_info.token_id.get_some(); + + let vinfo = if is_some == 1 { + if _is_erc721(execution_info.token_address) { + let nft_contract = IERC721Dispatcher { + contract_address: execution_info.token_address + }; + nft_contract + .transfer_from(execution_info.token_from, execution_info.token_to, token_id); + } + + if _is_erc1155(execution_info.token_address) { + let erc1155_contract = IERC1155Dispatcher { + contract_address: execution_info.token_address + }; + erc1155_contract + .safe_transfer_from( + execution_info.token_from, + execution_info.token_to, + token_id, + execution_info.token_quantity, + array![].span() + ); + } + + let tx_info = starknet::get_tx_info().unbox(); + let transaction_hash = tx_info.transaction_hash; + let block_timestamp = starknet::info::get_block_timestamp(); - if _is_erc1155(execution_info.token_address) { - let erc1155_contract = IERC1155Dispatcher { + ExecutionValidationInfo { + order_hash: execution_info.order_hash, + transaction_hash, + starknet_block_timestamp: block_timestamp, + from: execution_info.token_from, + to: execution_info.token_to, + } + } else { + let erc20_contract = IERC20Dispatcher { contract_address: execution_info.token_address }; - erc1155_contract - .safe_transfer_from( + erc20_contract + .transfer_from( execution_info.token_from, execution_info.token_to, - execution_info.token_id, - execution_info.token_quantity, - array![].span() + execution_info.token_quantity ); - } - let tx_info = starknet::get_tx_info().unbox(); - let transaction_hash = tx_info.transaction_hash; - let block_timestamp = starknet::info::get_block_timestamp(); + let tx_info = starknet::get_tx_info().unbox(); + let transaction_hash = tx_info.transaction_hash; + let block_timestamp = starknet::info::get_block_timestamp(); - let vinfo = ExecutionValidationInfo { - order_hash: execution_info.order_hash, - transaction_hash, - starknet_block_timestamp: block_timestamp, - from: execution_info.token_from, - to: execution_info.token_to, + ExecutionValidationInfo { + order_hash: execution_info.order_hash, + transaction_hash, + starknet_block_timestamp: block_timestamp, + from: execution_info.token_from, + to: execution_info.token_to, + } }; self.orderbook.validate_order_execution(vinfo); @@ -821,22 +935,30 @@ mod executor { } fn _compute_creator_fees_amount( - self: @ContractState, nft_address: @ContractAddress, payment_amount: u256, token_id: u256 + self: @ContractState, + token_address: @ContractAddress, + payment_amount: u256, + token_id: OptionU256 ) -> (ContractAddress, u256) { - // check if nft support 2981 interface - let dispatcher = ISRC5Dispatcher { contract_address: *nft_address }; - if dispatcher.supports_interface(IERC2981_ID) { - IERC2981Dispatcher { contract_address: *nft_address } - .royalty_info(token_id, payment_amount) + let (is_some, token_id) = token_id.get_some(); + if is_some == 0 { + _fallback_compute_creator_fees_amount(self, token_address, payment_amount) } else { - _fallback_compute_creator_fees_amount(self, nft_address, payment_amount) + // check if nft support 2981 interface + let dispatcher = ISRC5Dispatcher { contract_address: *token_address }; + if dispatcher.supports_interface(IERC2981_ID) { + IERC2981Dispatcher { contract_address: *token_address } + .royalty_info(token_id, payment_amount) + } else { + _fallback_compute_creator_fees_amount(self, token_address, payment_amount) + } } } fn _fallback_compute_creator_fees_amount( - self: @ContractState, nft_address: @ContractAddress, payment_amount: u256 + self: @ContractState, token_address: @ContractAddress, payment_amount: u256 ) -> (ContractAddress, u256) { - let (receiver, fees_ratio) = self.get_collection_creator_fees(*nft_address); + let (receiver, fees_ratio) = self.get_collection_creator_fees(*token_address); let amount = fees_ratio.compute_amount(payment_amount); (receiver, amount) } @@ -855,8 +977,8 @@ mod executor { self: @ContractState, fulfill_broker_address: ContractAddress, listing_broker_address: ContractAddress, - nft_address: ContractAddress, - nft_token_id: u256, + token_address: ContractAddress, + token_id: OptionU256, payment_amount: u256 ) -> (u256, u256, u256, u256) { let fulfill_broker_fees = self.get_broker_fees(fulfill_broker_address); @@ -866,7 +988,7 @@ mod executor { let fulfill_broker_fees_amount = fulfill_broker_fees.compute_amount(payment_amount); let listing_broker_fees_amount = listing_broker_fees.compute_amount(payment_amount); let (_, creator_fees_amount) = _compute_creator_fees_amount( - self, @nft_address, payment_amount, nft_token_id + self, @token_address, payment_amount, token_id, ); let ark_fees_amount = ark_fees.compute_amount(payment_amount); ( @@ -892,6 +1014,7 @@ mod executor { quantity: order.quantity, start_amount: order.start_amount, offerer: order.offerer, + route: order.route, } } } diff --git a/contracts/ark_starknet/src/interfaces.cairo b/contracts/ark_starknet/src/interfaces.cairo index f283272b..b4bc5c4d 100644 --- a/contracts/ark_starknet/src/interfaces.cairo +++ b/contracts/ark_starknet/src/interfaces.cairo @@ -1,6 +1,6 @@ use ark_common::protocol::order_types::ExecutionInfo; -use ark_common::protocol::order_types::OrderV1; use ark_common::protocol::order_types::{FulfillInfo, CancelInfo}; +use ark_common::protocol::order_types::{OrderV1, OptionU256, OptionU256Trait}; use ark_oz::erc2981::FeesRatio; //! Interfaces for arkchain operator. use starknet::{ClassHash, ContractAddress}; @@ -29,18 +29,21 @@ trait IExecutor { fn get_default_creator_fees(self: @T) -> (ContractAddress, FeesRatio); fn set_default_creator_fees(ref self: T, receiver: ContractAddress, fees_ratio: FeesRatio); fn get_collection_creator_fees( - self: @T, nft_address: ContractAddress + self: @T, token_address: ContractAddress ) -> (ContractAddress, FeesRatio); fn set_collection_creator_fees( - ref self: T, nft_address: ContractAddress, receiver: ContractAddress, fees_ratio: FeesRatio + ref self: T, + token_address: ContractAddress, + receiver: ContractAddress, + fees_ratio: FeesRatio ); fn get_fees_amount( self: @T, fulfill_broker: ContractAddress, listing_broker: ContractAddress, - nft_address: ContractAddress, - nft_token_id: u256, + token_address: ContractAddress, + token_id: OptionU256, payment_amount: u256 ) -> FeesAmount; } diff --git a/contracts/ark_starknet/tests/common/setup.cairo b/contracts/ark_starknet/tests/common/setup.cairo index bc8aab56..f129de3b 100644 --- a/contracts/ark_starknet/tests/common/setup.cairo +++ b/contracts/ark_starknet/tests/common/setup.cairo @@ -28,6 +28,20 @@ fn deploy_erc20() -> ContractAddress { erc20_address } +fn deploy_erc20_2() -> ContractAddress { + let contract = declare("TradeERC20").unwrap().contract_class(); + let initial_supply: u256 = 10_000_000_000_u256; + let name: ByteArray = "TradeERC20"; + let symbol: ByteArray = "TRADE"; + + let mut calldata: Array = array![]; + initial_supply.serialize(ref calldata); + name.serialize(ref calldata); + symbol.serialize(ref calldata); + let (erc20_address, _) = contract.deploy(@calldata).unwrap(); + erc20_address +} + fn deploy_nft(royalty: bool) -> ContractAddress { let name: ByteArray = "DummyNFT"; let symbol: ByteArray = "DUMNFT"; @@ -80,6 +94,12 @@ fn setup() -> (ContractAddress, ContractAddress, ContractAddress) { (executor_address, erc20_address, nft_address) } +fn setup_erc20_order() -> (ContractAddress, ContractAddress, ContractAddress) { + let erc20_address = deploy_erc20(); + let token_address = deploy_erc20_2(); + let executor_address = deploy_executor(); + (executor_address, erc20_address, token_address) +} fn setup_royalty() -> (ContractAddress, ContractAddress, ContractAddress) { let erc20_address = deploy_erc20(); @@ -103,6 +123,7 @@ fn setup_order( token_id: Option, start_amount: u256, end_amount: u256, + quantity: u256, ) -> OrderV1 { let chain_id = 'SN_MAIN'; let block_timestamp = starknet::get_block_timestamp(); @@ -118,7 +139,7 @@ fn setup_order( token_chain_id: chain_id, token_address: nft_address, token_id, - quantity: 1, + quantity: quantity, start_amount, end_amount, start_date: block_timestamp, @@ -142,7 +163,8 @@ fn setup_offer_order( offerer, Option::Some(token_id), start_amount, - 0 + 0, + 1 ) } @@ -160,7 +182,8 @@ fn setup_listing_order( offerer, Option::Some(token_id), start_amount, - 0 + 0, + 1 ) } @@ -179,7 +202,8 @@ fn setup_auction_order( offerer, Option::Some(token_id), start_amount, - end_amount + end_amount, + 1 ) } @@ -196,7 +220,46 @@ fn setup_collection_offer_order( offerer, Option::None, start_amount, - 0 + 0, + 1 + ) +} + +fn setup_limit_buy_order( + currency_address: ContractAddress, + token_address: ContractAddress, + offerer: ContractAddress, + start_amount: u256, + quantity: u256 +) -> OrderV1 { + setup_order( + currency_address, + token_address, + RouteType::Erc20ToErc20Buy, + offerer, + Option::None, + start_amount, + 0, + quantity + ) +} + +fn setup_limit_sell_order( + currency_address: ContractAddress, + token_address: ContractAddress, + offerer: ContractAddress, + end_amount: u256, + quantity: u256 +) -> OrderV1 { + setup_order( + currency_address, + token_address, + RouteType::Erc20ToErc20Sell, + offerer, + Option::None, + 0, + end_amount, + quantity ) } @@ -305,6 +368,28 @@ fn create_collection_offer_order( (order.compute_order_hash(), offerer, start_amount) } + +fn create_limit_buy_order( + executor_address: ContractAddress, + erc20_address: ContractAddress, + token_address: ContractAddress, + start_amount: u256, + quantity: u256 +) -> (felt252, ContractAddress, u256) { + let offerer = contract_address_const::<'offerer'>(); + + IFreeMintDispatcher { contract_address: erc20_address }.mint(offerer, start_amount); + + let order = setup_limit_buy_order( + erc20_address, token_address, offerer, start_amount, quantity + ); + + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.create_order(order); + + (order.compute_order_hash(), offerer, start_amount) +} + fn setup_order_erc1155( erc20_address: ContractAddress, erc1155_address: ContractAddress, quantity: u256 ) -> OrderV1 { @@ -356,3 +441,21 @@ fn create_offer_order_erc1155( (order.compute_order_hash(), offerer, start_amount) } +fn create_limit_sell_order( + executor_address: ContractAddress, + erc20_address: ContractAddress, + token_address: ContractAddress, + end_amount: u256, + quantity: u256 +) -> (felt252, ContractAddress, u256) { + let offerer = contract_address_const::<'offerer'>(); + + IFreeMintDispatcher { contract_address: token_address }.mint(offerer, quantity); + + let order = setup_limit_sell_order(erc20_address, token_address, offerer, end_amount, quantity); + + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.create_order(order); + + (order.compute_order_hash(), offerer, quantity) +} diff --git a/contracts/ark_starknet/tests/integration/cancel_order.cairo b/contracts/ark_starknet/tests/integration/cancel_order.cairo index 8e71daf9..d2252e0e 100644 --- a/contracts/ark_starknet/tests/integration/cancel_order.cairo +++ b/contracts/ark_starknet/tests/integration/cancel_order.cairo @@ -15,8 +15,9 @@ use snforge_std::{cheat_caller_address, CheatSpan, spy_events, EventSpyAssertion use starknet::{ContractAddress, contract_address_const}; use super::super::common::setup::{ create_auction_order, create_collection_offer_order, create_listing_order, create_offer_order, - setup, setup_default_order, setup_auction_order, setup_collection_offer_order, - setup_listing_order, setup_offer_order + setup, setup_erc20_order, setup_default_order, setup_auction_order, + setup_collection_offer_order, setup_listing_order, setup_offer_order, create_limit_buy_order, + create_limit_sell_order }; #[test] @@ -226,7 +227,113 @@ fn test_cancel_collection_offer_order() { } #[test] -// #[should_panic] +fn test_cancel_limit_buy_order() { + let (executor_address, erc20_address, token_address) = setup_erc20_order(); + let start_amount = 10_000_000; + let quantity = 5000; + + let (order_hash, offerer, _) = create_limit_buy_order( + executor_address, erc20_address, token_address, start_amount, quantity + ); + + let cancel_info = CancelInfo { + order_hash, + canceller: offerer, + token_chain_id: 'SN_MAIN', + token_address: token_address, + token_id: Option::None, + }; + + let mut spy = spy_events(); + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.cancel_order(cancel_info); + + spy + .assert_emitted( + @array![ + ( + executor_address, + executor::Event::OrderbookEvent( + OrderbookComponent::Event::OrderCancelled( + OrderbookComponent::OrderCancelled { + order_hash, + reason: OrderStatus::CancelledUser.into(), + order_type: OrderType::LimitBuy, + version: OrderbookComponent::ORDER_CANCELLED_EVENT_VERSION, + } + ) + ) + ) + ] + ); + + assert_eq!( + IOrderbookDispatcher { contract_address: executor_address }.get_order_type(order_hash), + OrderType::LimitBuy, + "Wrong order type" + ); + assert_eq!( + IOrderbookDispatcher { contract_address: executor_address }.get_order_status(order_hash), + OrderStatus::CancelledUser, + "Wrong order status" + ); +} + +#[test] +fn test_cancel_limit_sell_order() { + let (executor_address, erc20_address, token_address) = setup_erc20_order(); + let end_amount = 10_000_000; + let quantity = 5000; + + let (order_hash, offerer, _) = create_limit_sell_order( + executor_address, erc20_address, token_address, end_amount, quantity + ); + + let cancel_info = CancelInfo { + order_hash, + canceller: offerer, + token_chain_id: 'SN_MAIN', + token_address: token_address, + token_id: Option::None, + }; + + let mut spy = spy_events(); + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.cancel_order(cancel_info); + + spy + .assert_emitted( + @array![ + ( + executor_address, + executor::Event::OrderbookEvent( + OrderbookComponent::Event::OrderCancelled( + OrderbookComponent::OrderCancelled { + order_hash, + reason: OrderStatus::CancelledUser.into(), + order_type: OrderType::LimitSell, + version: OrderbookComponent::ORDER_CANCELLED_EVENT_VERSION, + } + ) + ) + ) + ] + ); + + assert_eq!( + IOrderbookDispatcher { contract_address: executor_address }.get_order_type(order_hash), + OrderType::LimitSell, + "Wrong order type" + ); + assert_eq!( + IOrderbookDispatcher { contract_address: executor_address }.get_order_status(order_hash), + OrderStatus::CancelledUser, + "Wrong order status" + ); +} + +#[test] +#[should_panic(expected: 'OB: order fulfilled')] fn test_cancel_offer_order_already_cancelled() { let (executor_address, erc20_address, nft_address) = setup(); let token_id = 10; diff --git a/contracts/ark_starknet/tests/integration/create_order.cairo b/contracts/ark_starknet/tests/integration/create_order.cairo index 1ae60481..08bb6740 100644 --- a/contracts/ark_starknet/tests/integration/create_order.cairo +++ b/contracts/ark_starknet/tests/integration/create_order.cairo @@ -25,8 +25,9 @@ use snforge_std::{ use starknet::{ContractAddress, contract_address_const}; use super::super::common::setup::{ - setup, setup_auction_order, setup_collection_offer_order, setup_listing_order, - setup_offer_order, setup_erc1155, setup_order_erc1155 + setup, setup_erc20_order, setup_auction_order, setup_collection_offer_order, + setup_listing_order, setup_offer_order, setup_limit_sell_order, setup_limit_buy_order, + setup_erc1155, setup_order_erc1155 }; // @@ -235,6 +236,106 @@ fn test_create_collection_offer_order_ok() { ); } +#[test] +fn test_create_limit_buy_order_ok() { + let (executor_address, erc20_address, token_address) = setup_erc20_order(); + let offerer = contract_address_const::<'offerer'>(); + let start_amount = 10_000_000; + let quantity = 5_000_000; + Erc20Dispatcher { contract_address: erc20_address }.mint(offerer, start_amount); + + let order = setup_limit_buy_order( + erc20_address, token_address, offerer, start_amount, quantity + ); + let order_hash = order.compute_order_hash(); + let order_version = order.get_version(); + + let mut spy = spy_events(); + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.create_order(order); + + spy + .assert_emitted( + @array![ + ( + executor_address, + executor::Event::OrderbookEvent( + OrderbookComponent::Event::OrderPlaced( + OrderbookComponent::OrderPlaced { + order_hash, + order_version, + order_type: OrderType::LimitBuy, + version: OrderbookComponent::ORDER_PLACED_EVENT_VERSION, + cancelled_order_hash: Option::None, + order, + } + ) + ) + ) + ] + ); + + assert_eq!( + IOrderbookDispatcher { contract_address: executor_address }.get_order_type(order_hash), + OrderType::LimitBuy, + "Wrong order type" + ); + assert_eq!( + IOrderbookDispatcher { contract_address: executor_address }.get_order_status(order_hash), + OrderStatus::Open, + "Wrong order status" + ); +} + +#[test] +fn test_create_limit_sell_order_ok() { + let (executor_address, erc20_address, token_address) = setup_erc20_order(); + let offerer = contract_address_const::<'offerer'>(); + let end_amount = 10_000_000; + let quantity = 5_000_000; + Erc20Dispatcher { contract_address: token_address }.mint(offerer, quantity); + + let order = setup_limit_sell_order(erc20_address, token_address, offerer, end_amount, quantity); + let order_hash = order.compute_order_hash(); + let order_version = order.get_version(); + + let mut spy = spy_events(); + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.create_order(order); + + spy + .assert_emitted( + @array![ + ( + executor_address, + executor::Event::OrderbookEvent( + OrderbookComponent::Event::OrderPlaced( + OrderbookComponent::OrderPlaced { + order_hash, + order_version, + order_type: OrderType::LimitSell, + version: OrderbookComponent::ORDER_PLACED_EVENT_VERSION, + cancelled_order_hash: Option::None, + order, + } + ) + ) + ) + ] + ); + + assert_eq!( + IOrderbookDispatcher { contract_address: executor_address }.get_order_type(order_hash), + OrderType::LimitSell, + "Wrong order type" + ); + assert_eq!( + IOrderbookDispatcher { contract_address: executor_address }.get_order_status(order_hash), + OrderStatus::Open, + "Wrong order status" + ); +} + #[test] #[should_panic(expected: "Caller is not the offerer")] fn test_create_offer_order_offerer_shall_be_caller() { @@ -479,6 +580,76 @@ fn test_create_collection_offer_order_expired() { stop_cheat_block_timestamp_global(); } +#[test] +#[should_panic(expected: "Oferrer does not own enough ERC20 tokens to buy")] +fn test_create_limit_buy_order_offerer_not_enough_erc20_tokens() { + let (executor_address, erc20_address, token_address) = setup_erc20_order(); + let offerer = contract_address_const::<'offerer'>(); + let start_amount = 10_000_000; + let minted = 10_000; + let quantity = 100_000; + + Erc20Dispatcher { contract_address: erc20_address }.mint(offerer, minted); + + let order = setup_limit_buy_order( + erc20_address, token_address, offerer, start_amount, quantity + ); + + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.create_order(order); +} + +#[test] +#[should_panic(expected: "Oferrer does not own enough ERC20 tokens to sell")] +fn test_create_limit_sell_order_offerer_not_enough_erc20_tokens() { + let (executor_address, erc20_address, token_address) = setup_erc20_order(); + let offerer = contract_address_const::<'offerer'>(); + let end_amount = 10_000_000; + let minted = 10_000; + let quantity = 100_000; + + Erc20Dispatcher { contract_address: token_address }.mint(offerer, minted); + + let order = setup_limit_sell_order(erc20_address, token_address, offerer, end_amount, quantity); + + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.create_order(order); +} + +#[test] +#[should_panic(expected: 'OB: order already exists')] +fn test_create_limit_buy_order_twice() { + let (executor_address, erc20_address, token_address) = setup_erc20_order(); + let offerer = contract_address_const::<'offerer'>(); + let start_amount = 10_000_000; + let quantity = 5_000_000; + Erc20Dispatcher { contract_address: erc20_address }.mint(offerer, start_amount); + + let order = setup_limit_buy_order( + erc20_address, token_address, offerer, start_amount, quantity + ); + + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(2)); + IExecutorDispatcher { contract_address: executor_address }.create_order(order); + IExecutorDispatcher { contract_address: executor_address }.create_order(order); +} + +#[test] +#[should_panic(expected: 'OB: order already exists')] +fn test_create_limit_sell_order_twice() { + let (executor_address, erc20_address, token_address) = setup_erc20_order(); + let offerer = contract_address_const::<'offerer'>(); + let end_amount = 10_000_000; + let quantity = 5_000_000; + Erc20Dispatcher { contract_address: token_address }.mint(offerer, quantity); + + let order = setup_limit_sell_order(erc20_address, token_address, offerer, end_amount, quantity); + + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(2)); + IExecutorDispatcher { contract_address: executor_address }.create_order(order); + IExecutorDispatcher { contract_address: executor_address }.create_order(order); +} + // // ERC1155 // @@ -523,7 +694,6 @@ fn test_create_order_erc1155_to_erc20_ok() { fn test_create_order_offerer_not_own_enough_erc1155_token() { let (executor_address, erc20_address, erc1155_address) = setup_erc1155(); let offerer = erc1155_address; - let other = contract_address_const::<'other'>(); let quantity = 50_u256; let token_id = Erc1155Dispatcher { contract_address: erc1155_address }.mint(offerer, 2_u256); diff --git a/contracts/ark_starknet/tests/integration/fees_amount.cairo b/contracts/ark_starknet/tests/integration/fees_amount.cairo index 1000e32b..740d5dbb 100644 --- a/contracts/ark_starknet/tests/integration/fees_amount.cairo +++ b/contracts/ark_starknet/tests/integration/fees_amount.cairo @@ -1,3 +1,4 @@ +use ark_common::protocol::order_types::{OptionU256, OptionU256Trait}; use ark_starknet::interfaces::{ IExecutorDispatcher, IExecutorDispatcherTrait, FeesAmount, FeesRatio }; @@ -34,7 +35,9 @@ fn test_get_fees_amount_default_creator() { executor.set_default_creator_fees(creator, default_creator_fees_ratio); let fees_amount = executor - .get_fees_amount(fulfill_broker, listing_broker, nft_address, 1, amount); + .get_fees_amount( + fulfill_broker, listing_broker, nft_address, OptionU256 { is_some: 0, value: 1 }, amount + ); assert_eq!(fees_amount.fulfill_broker, 1_000_000, "Wrong amount for fulfill broker"); assert_eq!(fees_amount.listing_broker, 500_000, "Wrong amount for listing broker"); @@ -71,7 +74,9 @@ fn test_get_fees_amount_collection_creator() { executor.set_collection_creator_fees(nft_address, creator, collection_creator_fees_ratio); let fees_amount = executor - .get_fees_amount(fulfill_broker, listing_broker, nft_address, 1, amount); + .get_fees_amount( + fulfill_broker, listing_broker, nft_address, OptionU256 { is_some: 0, value: 1 }, amount + ); assert_eq!(fees_amount.fulfill_broker, 1_000_000, "Wrong amount for fulfill broker"); assert_eq!(fees_amount.listing_broker, 500_000, "Wrong amount for listing broker"); diff --git a/contracts/ark_starknet/tests/integration/fulfill_order.cairo b/contracts/ark_starknet/tests/integration/fulfill_order.cairo index 21dae3f9..6a74ecb8 100644 --- a/contracts/ark_starknet/tests/integration/fulfill_order.cairo +++ b/contracts/ark_starknet/tests/integration/fulfill_order.cairo @@ -1,7 +1,6 @@ use ark_common::protocol::order_types::{FulfillInfo, OrderTrait, RouteType}; use ark_common::protocol::order_v1::OrderV1; - use ark_starknet::interfaces::{ IExecutorDispatcher, IExecutorDispatcherTrait, IMaintenanceDispatcher, IMaintenanceDispatcherTrait @@ -22,31 +21,18 @@ use starknet::{ContractAddress, contract_address_const}; use super::super::common::setup::{ create_auction_order, create_collection_offer_order, create_listing_order, create_offer_order, setup, setup_default_order, setup_auction_order, setup_collection_offer_order, - setup_listing_order, setup_offer_order, create_offer_order_erc1155, setup_erc1155 + setup_listing_order, setup_offer_order, setup_erc20_order, create_limit_buy_order, + create_limit_sell_order, setup_limit_sell_order, setup_limit_buy_order, + create_offer_order_erc1155, setup_erc1155 }; fn create_fulfill_info( - order_hash: felt252, fulfiller: ContractAddress, token_address: ContractAddress, token_id: u256 -) -> FulfillInfo { - FulfillInfo { - order_hash: order_hash, - related_order_hash: Option::None, - fulfiller: fulfiller, - token_chain_id: 'SN_MAIN', - token_address: token_address, - token_id: Option::Some(token_id), - quantity: 1_u256, - fulfill_broker_address: contract_address_const::<'broker'>() - } -} - -fn create_fulfill_info_erc1155( order_hash: felt252, fulfiller: ContractAddress, token_address: ContractAddress, - token_id: u256, - quantity: u256 + token_id: Option, + quantity: u256, ) -> FulfillInfo { FulfillInfo { order_hash: order_hash, @@ -54,12 +40,13 @@ fn create_fulfill_info_erc1155( fulfiller: fulfiller, token_chain_id: 'SN_MAIN', token_address: token_address, - token_id: Option::Some(token_id), + fulfill_broker_address: contract_address_const::<'broker'>(), quantity: quantity, - fulfill_broker_address: contract_address_const::<'broker'>() + token_id: token_id } } + #[test] fn test_fulfill_offer_order_ok() { let (executor_address, erc20_address, nft_address) = setup(); @@ -76,7 +63,9 @@ fn test_fulfill_offer_order_ok() { cheat_caller_address(erc20_address, offerer, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); - let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); + let fulfill_info = create_fulfill_info( + order_hash, fulfiller, nft_address, Option::Some(token_id), 1_u256 + ); cheat_caller_address(nft_address, fulfiller, CheatSpan::TargetCalls(1)); IERC721Dispatcher { contract_address: nft_address } @@ -102,7 +91,9 @@ fn test_fulfill_listing_order_ok() { IERC721Dispatcher { contract_address: nft_address } .set_approval_for_all(executor_address, true); - let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); + let fulfill_info = create_fulfill_info( + order_hash, fulfiller, nft_address, Option::Some(token_id), 1_u256 + ); cheat_caller_address(erc20_address, fulfiller, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); @@ -118,7 +109,7 @@ fn test_fulfill_order_fulfiller_shall_be_caller() { let caller = contract_address_const::<'caller'>(); let fulfiller = contract_address_const::<'fulfiller'>(); - let fulfill_info = create_fulfill_info(0x123, fulfiller, nft_address, 1); + let fulfill_info = create_fulfill_info(0x123, fulfiller, nft_address, Option::Some(1), 1_u256); cheat_caller_address(executor_address, caller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); @@ -137,7 +128,9 @@ fn test_fulfill_listing_order_fulfiller_not_enough_erc20_token() { IFreeMintDispatcher { contract_address: erc20_address }.mint(fulfiller, start_amount - 100); - let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); + let fulfill_info = create_fulfill_info( + order_hash, fulfiller, nft_address, Option::Some(token_id), 1_u256 + ); cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); @@ -158,7 +151,9 @@ fn test_fulfill_offer_order_fulfiller_not_owner_for_erc721() { executor_address, erc20_address, nft_address, token_id ); - let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); + let fulfill_info = create_fulfill_info( + order_hash, fulfiller, nft_address, Option::Some(token_id), 1_u256 + ); cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); @@ -178,8 +173,8 @@ fn test_fulfill_offer_order_fulfiller_not_owner_for_erc1155() { executor_address, erc20_address, erc1155_address, token_id, quantity ); - let fulfill_info = create_fulfill_info_erc1155( - order_hash, fulfiller, erc1155_address, token_id, quantity + let fulfill_info = create_fulfill_info( + order_hash, fulfiller, erc1155_address, Option::Some(token_id), quantity ); cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); @@ -192,7 +187,7 @@ fn test_fulfill_order_not_found() { let (executor_address, _erc20_address, nft_address) = setup(); let fulfiller = contract_address_const::<'fulfiller'>(); - let fulfill_info = create_fulfill_info(0x1234, fulfiller, nft_address, 1); + let fulfill_info = create_fulfill_info(0x1234, fulfiller, nft_address, Option::Some(1), 1_u256); cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); @@ -216,7 +211,9 @@ fn test_fulfill_offer_order_offerer_not_enough_allowance() { IERC20Dispatcher { contract_address: erc20_address } .approve(executor_address, start_amount - 10); - let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); + let fulfill_info = create_fulfill_info( + order_hash, fulfiller, nft_address, Option::Some(token_id), 1_u256 + ); cheat_caller_address(nft_address, fulfiller, CheatSpan::TargetCalls(1)); IERC721Dispatcher { contract_address: nft_address } @@ -243,7 +240,9 @@ fn test_fulfill_listing_order_fulfiller_not_enough_allowance() { IERC721Dispatcher { contract_address: nft_address } .set_approval_for_all(executor_address, true); - let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); + let fulfill_info = create_fulfill_info( + order_hash, fulfiller, nft_address, Option::Some(token_id), 1_u256 + ); cheat_caller_address(erc20_address, fulfiller, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address } @@ -266,7 +265,9 @@ fn test_fulfill_listing_order_offerer_not_approved() { IFreeMintDispatcher { contract_address: erc20_address }.mint(fulfiller, start_amount); - let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); + let fulfill_info = create_fulfill_info( + order_hash, fulfiller, nft_address, Option::Some(token_id), 1_u256 + ); cheat_caller_address(erc20_address, fulfiller, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); @@ -292,7 +293,9 @@ fn test_fulfill_offer_order_fulfiller_not_approved() { cheat_caller_address(erc20_address, offerer, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); - let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); + let fulfill_info = create_fulfill_info( + order_hash, fulfiller, nft_address, Option::Some(token_id), 1_u256 + ); cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); @@ -316,7 +319,9 @@ fn test_fulfill_offer_order_fulfiller_same_as_offerer() { cheat_caller_address(erc20_address, offerer, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); - let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); + let fulfill_info = create_fulfill_info( + order_hash, fulfiller, nft_address, Option::Some(token_id), 1_u256 + ); cheat_caller_address(nft_address, fulfiller, CheatSpan::TargetCalls(1)); IERC721Dispatcher { contract_address: nft_address } @@ -343,7 +348,9 @@ fn test_fulfill_listing_order_fulfiller_same_as_offerer() { IERC721Dispatcher { contract_address: nft_address } .set_approval_for_all(executor_address, true); - let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); + let fulfill_info = create_fulfill_info( + order_hash, fulfiller, nft_address, Option::Some(token_id), 1_u256 + ); cheat_caller_address(erc20_address, fulfiller, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); @@ -375,7 +382,9 @@ fn test_fulfill_auction_order_ok() { IERC721Dispatcher { contract_address: nft_address } .set_approval_for_all(executor_address, true); - let mut fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); + let mut fulfill_info = create_fulfill_info( + order_hash, fulfiller, nft_address, Option::Some(token_id), 1_u256 + ); fulfill_info.related_order_hash = Option::Some(buyer_order.compute_order_hash()); cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(1)); @@ -385,6 +394,7 @@ fn test_fulfill_auction_order_ok() { IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); } + #[test] fn test_fulfill_auction_order_fulfiller_same_as_offerer() { let (executor_address, erc20_address, nft_address) = setup(); @@ -408,7 +418,9 @@ fn test_fulfill_auction_order_fulfiller_same_as_offerer() { IERC721Dispatcher { contract_address: nft_address } .set_approval_for_all(executor_address, true); - let mut fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); + let mut fulfill_info = create_fulfill_info( + order_hash, fulfiller, nft_address, Option::Some(token_id), 1_u256 + ); fulfill_info.related_order_hash = Option::Some(buyer_order.compute_order_hash()); cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(1)); @@ -436,7 +448,9 @@ fn test_fulfill_order_not_enabled() { cheat_caller_address(erc20_address, offerer, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); - let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); + let fulfill_info = create_fulfill_info( + order_hash, fulfiller, nft_address, Option::Some(token_id), 1_u256 + ); cheat_caller_address(nft_address, fulfiller, CheatSpan::TargetCalls(1)); IERC721Dispatcher { contract_address: nft_address } @@ -448,3 +462,210 @@ fn test_fulfill_order_not_enabled() { cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); } + + +#[test] +fn test_fulfill_limit_buy_order_ok() { + let (executor_address, erc20_address, token_address) = setup_erc20_order(); + let start_amount = 10_000_000; + let quantity = 20_000; + + let (order_hash, buyer, _) = create_limit_buy_order( + executor_address, erc20_address, token_address, start_amount, quantity + ); + + let fulfiller = buyer; + + let seller = contract_address_const::<'seller'>(); + + IFreeMintDispatcher { contract_address: token_address }.mint(seller, quantity); + + let seller_order = setup_limit_sell_order( + erc20_address, token_address, seller, start_amount, quantity + ); + + cheat_caller_address(executor_address, seller, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.create_order(seller_order); + + // approve executor + cheat_caller_address(token_address, seller, CheatSpan::TargetCalls(1)); + IERC20Dispatcher { contract_address: token_address }.approve(executor_address, quantity); + + // approve executor + cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(1)); + IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); + + let mut fulfill_info = create_fulfill_info( + order_hash, fulfiller, token_address, Option::None, quantity + ); + fulfill_info.related_order_hash = Option::Some(seller_order.compute_order_hash()); + + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); +} + +#[test] +fn test_fulfill_limit_sell_order_ok() { + let (executor_address, erc20_address, token_address) = setup_erc20_order(); + let end_amount = 50_000_000; + let start_amount = end_amount; + let quantity = 20_000_000; + + let (order_hash, seller, _) = create_limit_sell_order( + executor_address, erc20_address, token_address, end_amount, quantity + ); + + let fulfiller = seller; + + let buyer = contract_address_const::<'buyer'>(); + + IFreeMintDispatcher { contract_address: erc20_address }.mint(buyer, start_amount); + + let buyer_order = setup_limit_buy_order( + erc20_address, token_address, buyer, start_amount, quantity + ); + + cheat_caller_address(executor_address, buyer, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.create_order(buyer_order); + + // approve executor + cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(1)); + IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); + + // approve executor + cheat_caller_address(token_address, seller, CheatSpan::TargetCalls(1)); + IERC20Dispatcher { contract_address: token_address }.approve(executor_address, quantity); + + let mut fulfill_info = create_fulfill_info( + order_hash, fulfiller, token_address, Option::None, quantity + ); + fulfill_info.related_order_hash = Option::Some(buyer_order.compute_order_hash()); + + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); +} + +#[test] +#[should_panic(expected: 'Order route not valid')] +fn test_fulfill_limit_buy_order_with_buy_order() { + let (executor_address, erc20_address, token_address) = setup_erc20_order(); + let start_amount = 10_000_000; + let quantity = 20_000; + + let (order_hash, buyer, _) = create_limit_buy_order( + executor_address, erc20_address, token_address, start_amount, quantity + ); + + let fulfiller = buyer; + + let wrong_seller = contract_address_const::<'seller'>(); + + IFreeMintDispatcher { contract_address: erc20_address }.mint(wrong_seller, start_amount); + + let wrong_order = setup_limit_buy_order( + erc20_address, token_address, wrong_seller, start_amount, quantity + ); + + cheat_caller_address(executor_address, wrong_seller, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.create_order(wrong_order); + + // approve executor + cheat_caller_address(erc20_address, wrong_seller, CheatSpan::TargetCalls(1)); + IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); + + // approve executor + cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(1)); + IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); + + let mut fulfill_info = create_fulfill_info( + order_hash, fulfiller, token_address, Option::None, quantity + ); + fulfill_info.related_order_hash = Option::Some(wrong_order.compute_order_hash()); + + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); +} + + +#[test] +#[should_panic(expected: 'Order route not valid')] +fn test_fulfill_limit_sell_order_with_sell_order_ok() { + let (executor_address, erc20_address, token_address) = setup_erc20_order(); + let end_amount = 50_000_000; + let quantity = 20_000_000; + + let (order_hash, seller, _) = create_limit_sell_order( + executor_address, erc20_address, token_address, end_amount, quantity + ); + + let fulfiller = seller; + + let wrong_buyer = contract_address_const::<'buyer'>(); + + IFreeMintDispatcher { contract_address: token_address }.mint(wrong_buyer, quantity); + + let wrong_order = setup_limit_sell_order( + erc20_address, token_address, wrong_buyer, end_amount, quantity + ); + + cheat_caller_address(executor_address, wrong_buyer, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.create_order(wrong_order); + + // approve executor + cheat_caller_address(token_address, wrong_buyer, CheatSpan::TargetCalls(1)); + IERC20Dispatcher { contract_address: token_address }.approve(executor_address, quantity); + + // approve executor + cheat_caller_address(token_address, seller, CheatSpan::TargetCalls(1)); + IERC20Dispatcher { contract_address: token_address }.approve(executor_address, quantity); + + let mut fulfill_info = create_fulfill_info( + order_hash, fulfiller, token_address, Option::None, quantity + ); + fulfill_info.related_order_hash = Option::Some(wrong_order.compute_order_hash()); + + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); +} + +#[test] +#[should_panic(expected: 'OB: order price not match')] +fn test_fulfill_limit_order_without_matching_price_ok() { + let (executor_address, erc20_address, token_address) = setup_erc20_order(); + let end_amount = 50_000_000; + let start_amount = 100_000_000; + let quantity = 20_000_000; + + let (order_hash, seller, _) = create_limit_sell_order( + executor_address, erc20_address, token_address, end_amount, quantity + ); + + let fulfiller = seller; + + let buyer = contract_address_const::<'buyer'>(); + + IFreeMintDispatcher { contract_address: erc20_address }.mint(buyer, start_amount); + + let buyer_order = setup_limit_buy_order( + erc20_address, token_address, buyer, start_amount, quantity + ); + + cheat_caller_address(executor_address, buyer, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.create_order(buyer_order); + + // approve executor + cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(1)); + IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); + + // approve executor + cheat_caller_address(token_address, seller, CheatSpan::TargetCalls(1)); + IERC20Dispatcher { contract_address: token_address }.approve(executor_address, quantity); + + let mut fulfill_info = create_fulfill_info( + order_hash, fulfiller, token_address, Option::None, quantity + ); + fulfill_info.related_order_hash = Option::Some(buyer_order.compute_order_hash()); + + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); + IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); +} diff --git a/contracts/ark_tokens/src/erc20_trade.cairo b/contracts/ark_tokens/src/erc20_trade.cairo new file mode 100644 index 00000000..e5603b16 --- /dev/null +++ b/contracts/ark_tokens/src/erc20_trade.cairo @@ -0,0 +1,54 @@ +use starknet::ContractAddress; + +#[starknet::interface] +trait IFreeMint { + fn mint(ref self: T, recipient: ContractAddress, amount: u256); +} + +#[starknet::contract] +mod TradeERC20 { + use openzeppelin::token::erc20::ERC20Component; + use openzeppelin::token::erc20::ERC20HooksEmptyImpl; + use starknet::ContractAddress; + use super::IFreeMint; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + #[abi(embed_v0)] + impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; + #[abi(embed_v0)] + impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + initial_supply: u256, + name: core::byte_array::ByteArray, + symbol: core::byte_array::ByteArray + ) { + self.erc20.initializer(name, symbol); + } + + #[abi(embed_v0)] + impl ImplFreeMint of IFreeMint { + fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { + self.erc20.mint(recipient, amount); + } + } +} diff --git a/contracts/ark_tokens/src/lib.cairo b/contracts/ark_tokens/src/lib.cairo index a0c97edf..1d13512d 100644 --- a/contracts/ark_tokens/src/lib.cairo +++ b/contracts/ark_tokens/src/lib.cairo @@ -1,5 +1,5 @@ mod erc1155; mod erc20; +mod erc20_trade; mod erc721; mod erc721_royalty; - diff --git a/packages/deployer/scripts/deploy.ts b/packages/deployer/scripts/deploy.ts index da829eb3..d35a4c1a 100644 --- a/packages/deployer/scripts/deploy.ts +++ b/packages/deployer/scripts/deploy.ts @@ -5,11 +5,12 @@ import { Account, RpcProvider } from "starknet"; import { ARTIFACTS_PATH } from "../src/constants"; import { deployERC20 } from "../src/contracts/erc20"; +import { deployERC20Trade } from "../src/contracts/erc20trade"; import { deployERC721 } from "../src/contracts/erc721"; import { deployERC721Royalties } from "../src/contracts/erc721royalties"; +import { deployERC1155 } from "../src/contracts/erc1155"; import { deployExecutor } from "../src/contracts/executor"; import { getFeeAddress } from "../src/providers"; -import { deployERC1155 } from "../src/contracts/erc1155"; async function run() { if ( @@ -45,6 +46,14 @@ async function run() { "ETH" ); + const ethTradeContract = await deployERC20Trade( + ARTIFACTS_PATH, + starknetAdminAccount, + provider, + "ETH", + "ETH" + ); + const nftContract = await deployERC721( ARTIFACTS_PATH, starknetAdminAccount, @@ -81,7 +90,8 @@ async function run() { nftContractFixedFees: nftContractFixedFees.address, nftContractRoyalties: nftContractRoyalties.address, eth: ethContract.address, - erc1155: erc1155Contract.address, + ethTrade: ethTradeContract.address, + erc1155: erc1155Contract.address }); await fs.writeFile(contractsFilePath, contractsContent); } diff --git a/packages/deployer/src/contracts/erc20trade.ts b/packages/deployer/src/contracts/erc20trade.ts new file mode 100644 index 00000000..f1c3f2fe --- /dev/null +++ b/packages/deployer/src/contracts/erc20trade.ts @@ -0,0 +1,40 @@ +import * as sn from "starknet"; + +import { loadArtifacts } from "./common"; + +/** + * Declare and deploys orderbook contract. + * Returns the contract object. + */ +export async function deployERC20Trade( + artifactsPath: string, + account: sn.Account, + provider: sn.RpcProvider, + name: string, + symbol: string +): Promise { + const artifacts = loadArtifacts(artifactsPath, "ark_tokens_TradeERC20"); + const contractCallData = new sn.CallData(artifacts.sierra.abi); + const contractConstructor = contractCallData.compile("constructor", { + initial_supply: sn.cairo.uint256(0), + name, + symbol + }); + + const deployR = await account.declareAndDeploy({ + contract: artifacts.sierra, + casm: artifacts.casm, + constructorCalldata: contractConstructor, + salt: "1337" + }); + + if (deployR.declare.transaction_hash) { + await provider.waitForTransaction(deployR.declare.transaction_hash); + } + + return new sn.Contract( + artifacts.sierra.abi, + deployR.deploy.contract_address, + provider + ); +}