diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..b43ca34a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,10 @@ +{ + "name": "Satoru", + "image": "mcr.microsoft.com/devcontainers/base:jammy", + "customizations": { + "vscode": { + "extensions": ["starkware.cairo1"] + } + }, + "postCreateCommand": "curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh && curl -L https://raw.githubusercontent.com/foundry-rs/starknet-foundry/master/scripts/install.sh | sh -s -- -v 0.7.0" +} diff --git a/book/src/getting-started/prerequisites.md b/book/src/getting-started/prerequisites.md index 8613d429..c569ce3b 100644 --- a/book/src/getting-started/prerequisites.md +++ b/book/src/getting-started/prerequisites.md @@ -1,3 +1,14 @@ # Prerequisites +There are several ways to run Satoru: +- Install prerequisites on the local system +- Run in a dev container + +## Installation on the local system +- [Scarb](https://docs.swmansion.com/scarb/download.html) - [Starknet Foundry](https://foundry-rs.github.io/starknet-foundry/) + +## Run in a dev container +Dev containers provide a dedicated environment for the project. Since the dev container configuration is stored in the `.devcontainer` directory, this ensures that the environment is strictly identical from one developer to the next. + +To run inside a dev container, please follow [Dev Containers tutorial](https://code.visualstudio.com/docs/devcontainers/tutorial). \ No newline at end of file diff --git a/src/event/event_utils.cairo b/src/event/event_utils.cairo index 4b4cc7d1..a4c839bd 100644 --- a/src/event/event_utils.cairo +++ b/src/event/event_utils.cairo @@ -161,97 +161,134 @@ struct StringArrayKeyValue { fn set_item_address_items( mut items: AddressItems, index: u32, key: felt252, value: ContractAddress -) { +) -> AddressItems { let address_key_value: AddressKeyValue = AddressKeyValue { key, value }; - items.items.append(address_key_value); + let mut address: AddressItems = items; + address.items.append(address_key_value); + return address; } fn set_item_array_address_items( mut items: AddressItems, index: u32, key: felt252, value: Array -) { +) -> AddressItems { let address_array_key_value: AddressArrayKeyValue = AddressArrayKeyValue { key, value }; - items.array_items.append(address_array_key_value); + let mut array_address: AddressItems = items; + array_address.array_items.append(address_array_key_value); + return array_address; } // Uint -fn set_item_uint_items(mut items: UintItems, index: u32, key: felt252, value: u128) { +fn set_item_uint_items(mut items: UintItems, index: u32, key: felt252, value: u128) -> UintItems { let uint_key_value: UintKeyValue = UintKeyValue { key, value }; - items.items.append(uint_key_value); + let mut address: UintItems = items; + address.items.append(uint_key_value); + return address; } -fn set_item_array_uint_items(mut items: UintItems, index: u32, key: felt252, value: Array) { +fn set_item_array_uint_items( + mut items: UintItems, index: u32, key: felt252, value: Array +) -> UintItems { let uint_array_key_value: UintArrayKeyValue = UintArrayKeyValue { key, value }; - items.array_items.append(uint_array_key_value); + let mut array_address: UintItems = items; + array_address.array_items.append(uint_array_key_value); + return array_address; } // in128 -fn set_item_int_items(mut items: IntItems, index: u32, key: felt252, value: i128) { +fn set_item_int_items(mut items: IntItems, index: u32, key: felt252, value: i128) -> IntItems { let int_key_value: IntKeyValue = IntKeyValue { key, value }; - // items.items.set(index, int_key_value); - items.items.append(int_key_value); + let mut address: IntItems = items; + address.items.append(int_key_value); + return address; } -fn set_item_array_int_items(mut items: IntItems, index: u32, key: felt252, value: Array) { +fn set_item_array_int_items( + mut items: IntItems, index: u32, key: felt252, value: Array +) -> IntItems { let int_array_key_value: IntArrayKeyValue = IntArrayKeyValue { key, value }; - items.array_items.append(int_array_key_value); + let mut array_address: IntItems = items; + array_address.array_items.append(int_array_key_value); + return array_address; } // bool -fn set_item_bool_items(mut items: BoolItems, index: u32, key: felt252, value: bool) { +fn set_item_bool_items(mut items: BoolItems, index: u32, key: felt252, value: bool) -> BoolItems { let bool_key_value: BoolKeyValue = BoolKeyValue { key, value }; - items.items.append(bool_key_value); + let mut address: BoolItems = items; + address.items.append(bool_key_value); + return address; } -fn set_item_array_bool_items(mut items: BoolItems, index: u32, key: felt252, value: Array) { +fn set_item_array_bool_items( + mut items: BoolItems, index: u32, key: felt252, value: Array +) -> BoolItems { let bool_array_key_value: BoolArrayKeyValue = BoolArrayKeyValue { key, value }; - items.array_items.append(bool_array_key_value); + let mut array_address: BoolItems = items; + array_address.array_items.append(bool_array_key_value); + return array_address; } // felt252 -fn set_item_Felt252_items(mut items: Felt252Items, index: u32, key: felt252, value: felt252) { - let Felt252_key_value: Felt252KeyValue = Felt252KeyValue { key, value }; - items.items.append(Felt252_key_value); +fn set_item_Felt252_items( + mut items: Felt252Items, index: u32, key: felt252, value: felt252 +) -> Felt252Items { + let felt252_key_value: Felt252KeyValue = Felt252KeyValue { key, value }; + let mut address: Felt252Items = items; + address.items.append(felt252_key_value); + return address; } fn set_item_array_Felt252_items( mut items: Felt252Items, index: u32, key: felt252, value: Array -) { - let Felt252_array_key_value: Felt252ArrayKeyValue = Felt252ArrayKeyValue { key, value }; - items.array_items.append(Felt252_array_key_value); +) -> Felt252Items { + let felt252_array_key_value: Felt252ArrayKeyValue = Felt252ArrayKeyValue { key, value }; + let mut array_address: Felt252Items = items; + array_address.array_items.append(felt252_array_key_value); + return array_address; } // array of felt fn set_item_array_of_felt_items_items( mut items: ArrayOfFeltItems, index: u32, key: felt252, value: Array -) { +) -> ArrayOfFeltItems { let array_of_felt_items_key_value: ArrayOfFeltKeyValue = ArrayOfFeltKeyValue { key, value }; - items.items.append(array_of_felt_items_key_value); + let mut address: ArrayOfFeltItems = items; + address.items.append(array_of_felt_items_key_value); + return address; } fn set_item_array_array_of_felt_items( mut items: ArrayOfFeltItems, index: u32, key: felt252, value: Array> -) { +) -> ArrayOfFeltItems { let array_of_felt_array_key_value: ArrayOfFeltArrayKeyValue = ArrayOfFeltArrayKeyValue { key, value }; - items.array_items.append(array_of_felt_array_key_value); + let mut array_address: ArrayOfFeltItems = items; + array_address.array_items.append(array_of_felt_array_key_value); + return array_address; } // string -fn set_item_string_items(mut items: StringItems, index: u32, key: felt252, value: felt252) { +fn set_item_string_items( + mut items: StringItems, index: u32, key: felt252, value: felt252 +) -> StringItems { let string_key_value: StringKeyValue = StringKeyValue { key, value }; - items.items.append(string_key_value); + let mut address: StringItems = items; + address.items.append(string_key_value); + return address; } fn set_item_array_string_items( mut items: StringItems, index: u32, key: felt252, value: Array -) { +) -> StringItems { let string_array_key_value: StringArrayKeyValue = StringArrayKeyValue { key, value }; - items.array_items.append(string_array_key_value); + let mut array_address: StringItems = items; + array_address.array_items.append(string_array_key_value); + return array_address; } diff --git a/src/lib.cairo b/src/lib.cairo index 8b11aa79..f26fb239 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -198,6 +198,7 @@ mod order { mod order_store_utils; mod order_event_utils; mod error; + mod swap_order_utils; } // `position` contains positions management functions diff --git a/src/order/decrease_order_utils.cairo b/src/order/decrease_order_utils.cairo index b9c4eb01..9f22471e 100644 --- a/src/order/decrease_order_utils.cairo +++ b/src/order/decrease_order_utils.cairo @@ -1,15 +1,137 @@ // Core lib imports. -use starknet::ContractAddress; +use starknet::{ContractAddress, contract_address_const}; // Local imports. use satoru::oracle::oracle::{IOracleDispatcher, IOracleDispatcherTrait}; +use satoru::data::data_store::{IDataStoreDispatcher, IDataStoreDispatcherTrait}; +use satoru::event::event_emitter::{IEventEmitterDispatcher, IEventEmitterDispatcherTrait}; +use satoru::oracle::oracle_utils; use satoru::position::decrease_position_utils::DecreasePositionResult; -use satoru::order::{base_order_utils::ExecuteOrderParams, order::Order}; +use satoru::position::decrease_position_utils; +use satoru::order::{ + base_order_utils::ExecuteOrderParams, order::Order, order::OrderType, error::OrderError, order +}; +use satoru::bank::bank::{IBankDispatcher, IBankDispatcherTrait}; + +use satoru::utils::arrays; +use satoru::market::market_utils; +use satoru::position::position_utils; +use satoru::position::position::Position; +use satoru::swap::swap_utils::{SwapParams}; +use satoru::position::position_utils::UpdatePositionParams; +use satoru::event::event_utils::LogData; +use satoru::event::event_utils; +use satoru::market::market_token::{IMarketTokenDispatcher, IMarketTokenDispatcherTrait}; +use satoru::utils::span32::{Span32, Array32Trait}; +use satoru::swap::swap_handler::{ISwapHandlerDispatcher, ISwapHandlerDispatcherTrait}; + // This function should return an EventLogData cause the callback_utils // needs it. We need to find a solution for that case. #[inline(always)] -fn process_order(params: ExecuteOrderParams) { //TODO +fn process_order( + params: ExecuteOrderParams +) -> event_utils::LogData { //TODO check with refactor with callback_utils + let order: Order = params.order; + + market_utils::validate_position_market_check(params.contracts.data_store, params.market); + + let position_key: felt252 = position_utils::get_position_key( + order.account, order.market, order.initial_collateral_token, order.is_long + ); + + let data_store: IDataStoreDispatcher = params.contracts.data_store; + let position_result = data_store.get_position(position_key); + let mut position: Position = Default::default(); + + match position_result { + Option::Some(pos) => { + position = pos; + }, + Option::None => { + panic_with_felt252(OrderError::POSTION_NOT_VALID); + } + } + + position_utils::validate_non_empty_position(position); + + validate_oracle_block_numbers( + params.min_oracle_block_numbers.span(), + params.max_oracle_block_numbers.span(), + order.order_type, + order.updated_at_block, + position.increased_at_block, + position.decreased_at_block + ); + + let mut update_position_params: UpdatePositionParams = UpdatePositionParams { + contracts: params.contracts, + market: params.market, + order: order, + order_key: params.key, + position: position, + position_key, + secondary_order_type: params.secondary_order_type + }; + + let mut result: DecreasePositionResult = decrease_position_utils::decrease_position( + ref update_position_params + ); + + // if the pnl_token and the collateral_token are different + // and if a swap fails or no swap was requested + // then it is possible to receive two separate tokens from decreasing + // the position + // transfer the two tokens to the user in this case and skip processing + // the swap_path + if (result.secondary_output_amount > 0) { + validate_output_amount_secondary( + params.contracts.oracle, + result.output_token, + result.output_amount, + result.secondary_output_token, + result.secondary_output_amount, + order.min_output_amount + ); + + IMarketTokenDispatcher { contract_address: order.market } + .transfer_out(result.output_token, order.receiver, result.output_amount); + + IMarketTokenDispatcher { contract_address: order.market } + .transfer_out( + result.secondary_output_token, order.receiver, result.secondary_output_amount + ); + + return get_output_event_data( + result.output_token, + result.output_amount, + result.secondary_output_token, + result.secondary_output_amount + ); + } + + let swap_param: SwapParams = SwapParams { + data_store: params.contracts.data_store, + event_emitter: params.contracts.event_emitter, + oracle: params.contracts.oracle, + bank: IBankDispatcher { contract_address: order.market }, + key: params.key, + token_in: result.output_token, + amount_in: result.output_amount, + swap_path_markets: params.swap_path_markets.span(), + min_output_amount: 0, + receiver: order.receiver, + ui_fee_receiver: order.ui_fee_receiver, + }; + + //TODO handle the swap_error when its possible + let (token_out, swap_output_amount) = params.contracts.swap_handler.swap(swap_param); + + validate_output_amount( + params.contracts.oracle, token_out, swap_output_amount, order.min_output_amount + ); + + return get_output_event_data(token_out, swap_output_amount, contract_address_const::<0>(), 0); } @@ -23,13 +145,45 @@ fn process_order(params: ExecuteOrderParams) { //TODO /// * `position_decrease_at_block` - The block at which the position was last decreased. #[inline(always)] fn validate_oracle_block_numbers( - min_oracle_block_numbers: Array, - max_oracle_block_numbers: Array, - order_type: Order, - order_updated_at_block: u128, - position_increased_at_block: u128, - position_decrease_at_block: u128 -) { //TODO + min_oracle_block_numbers: Span, + max_oracle_block_numbers: Span, + order_type: OrderType, + order_updated_at_block: u64, + position_increased_at_block: u64, + position_decreased_at_block: u64 +) { + if order_type == OrderType::MarketDecrease { + oracle_utils::validate_block_number_within_range( + min_oracle_block_numbers, max_oracle_block_numbers, order_updated_at_block + ); + return; + } + + if (order_type == OrderType::LimitDecrease || order_type == OrderType::StopLossDecrease) { + let mut latest_updated_at_block: u64 = position_increased_at_block; + if (order_updated_at_block > position_increased_at_block) { + latest_updated_at_block = order_updated_at_block + } + if (!arrays::u64_are_gte(min_oracle_block_numbers, latest_updated_at_block)) { + OrderError::ORACLE_BLOCK_NUMBERS_ARE_SMALLER_THAN_REQUIRED( + min_oracle_block_numbers, latest_updated_at_block + ); + } + return; + } + if (order_type == OrderType::Liquidation) { + let mut latest_updated_at_block: u64 = position_decreased_at_block; + if (position_increased_at_block > position_decreased_at_block) { + latest_updated_at_block = position_increased_at_block + } + if (!arrays::u64_are_gte(min_oracle_block_numbers, latest_updated_at_block)) { + OrderError::ORACLE_BLOCK_NUMBERS_ARE_SMALLER_THAN_REQUIRED( + min_oracle_block_numbers, latest_updated_at_block + ); + } + return; + } + panic_with_felt252(OrderError::UNSUPPORTED_ORDER_TYPE); } // Note: that min_output_amount is treated as a USD value for this validation @@ -37,9 +191,14 @@ fn validate_output_amount( oracle: IOracleDispatcher, output_token: ContractAddress, output_amount: u128, - min_output_amount: u128, - max_output_amount: u128 -) { //TODO + min_output_amount: u128 +) { + let output_token_price: u128 = oracle.get_primary_price(output_token).min; + let output_usd: u128 = output_amount * output_token_price; + + if (output_usd < min_output_amount) { + OrderError::INSUFFICIENT_OUTPUT_AMOUNT(output_usd, output_token_price); + } } // Note: that min_output_amount is treated as a USD value for this validation @@ -47,11 +206,21 @@ fn validate_output_amount_secondary( oracle: IOracleDispatcher, output_token: ContractAddress, output_amount: u128, - min_output_amount: u128, - secondary_output_token: u128, + secondary_output_token: ContractAddress, secondary_output_amount: u128, - max_output_amount: u128 -) { //TODO + min_output_amount: u128 +) { + let output_token_price: u128 = oracle.get_primary_price(output_token).min; + let output_usd: u128 = output_amount * output_token_price; + + let secondary_output_token_price: u128 = oracle.get_primary_price(secondary_output_token).min; + let seconday_output_usd: u128 = secondary_output_amount * secondary_output_token_price; + + let total_output_usd: u128 = output_usd + seconday_output_usd; + + if (total_output_usd < min_output_amount) { + OrderError::INSUFFICIENT_OUTPUT_AMOUNT(output_usd, output_token_price); + } } #[inline(always)] @@ -60,8 +229,17 @@ fn handle_swap_error( order: Order, result: DecreasePositionResult, reason: felt252, - reason_bytes: Array -) { //TOO + reason_bytes: Span, + event_emitter: IEventEmitterDispatcher +) { + event_emitter.emit_swap_reverted(reason, reason_bytes); + + validate_output_amount( + oracle, result.output_token, result.output_amount, order.min_output_amount + ); + + IMarketTokenDispatcher { contract_address: order.market } + .transfer_out(result.output_token, order.receiver, result.output_amount); } // This function should return an EventLogData cause the callback_utils @@ -71,5 +249,30 @@ fn get_output_event_data( output_amount: u128, secondary_output_token: ContractAddress, secondary_output_amount: u128 -) { //TODO +) -> event_utils::LogData { + let mut address_items: event_utils::AddressItems = Default::default(); + let mut uint_items: event_utils::UintItems = Default::default(); + + address_items = + event_utils::set_item_address_items(address_items, 0, "output_token", output_token); + address_items = + event_utils::set_item_address_items( + address_items, 1, "secondary_output_token", secondary_output_token + ); + + uint_items = event_utils::set_item_uint_items(uint_items, 0, "output_amount", output_amount); + uint_items = + event_utils::set_item_uint_items( + uint_items, 1, "secondary_output_amount", secondary_output_amount + ); + + event_utils::LogData { + address_items, + uint_items, + int_items: Default::default(), + bool_items: Default::default(), + felt252_items: Default::default(), + array_of_felt_items: Default::default(), + string_items: Default::default(), + } } diff --git a/src/order/error.cairo b/src/order/error.cairo index c8b06797..75e1c472 100644 --- a/src/order/error.cairo +++ b/src/order/error.cairo @@ -11,6 +11,35 @@ mod OrderError { const ORDER_INDEX_NOT_FOUND: felt252 = 'order_index_not_found'; const CANT_BE_ZERO: felt252 = 'order account cant be 0'; const EMPTY_SIZE_DELTA_IN_TOKENS: felt252 = 'empty_size_delta_in_tokens'; + const UNEXPECTED_MARKET: felt252 = 'unexpected market'; + const INVALID_SIZE_DELTA_FOR_ADL: felt252 = 'invalid_size_delta_for_adl'; + const POSTION_NOT_VALID: felt252 = 'position_not_valid'; + + + fn ORACLE_BLOCK_NUMBERS_ARE_SMALLER_THAN_REQUIRED( + min_oracle_block_numbers: Span, latest_updated_at_block: u64 + ) { + let mut data: Array = array!['Block nbs smaller than required']; + let len: u32 = min_oracle_block_numbers.len(); + let mut i: u32 = 0; + loop { + if (i == len) { + break; + } + let value: u64 = *min_oracle_block_numbers.at(i); + data.append(value.into()); + i += 1; + }; + data.append(latest_updated_at_block.into()); + panic(data) + } + + fn INSUFFICIENT_OUTPUT_AMOUNT(output_usd: u128, min_output_amount: u128) { + let mut data = array!['Insufficient output amount']; + data.append(output_usd.into()); + data.append(min_output_amount.into()); + panic(data); + } fn INVALID_ORDER_PRICE(primary_price: Price, trigger_price: u128, order_type: OrderType) { let mut data: Array = array![]; diff --git a/src/order/swap_order_utils.cairo b/src/order/swap_order_utils.cairo index 702d1068..11f86069 100644 --- a/src/order/swap_order_utils.cairo +++ b/src/order/swap_order_utils.cairo @@ -4,11 +4,63 @@ use starknet::ContractAddress; // Local imports. use satoru::order::base_order_utils::ExecuteOrderParams; use satoru::order::order::OrderType; +use satoru::oracle::oracle_utils; +use satoru::utils::arrays::u64_are_gte; +use satoru::swap::swap_utils; +use satoru::event::event_utils; +use satoru::order::error::OrderError; +use satoru::bank::bank::{IBankDispatcher, IBankDispatcherTrait}; +use satoru::utils::span32::{Span32, DefaultSpan32}; +use satoru::oracle::error::OracleError; -// This function should return an EventLogData cause the callback_utils -// needs it. We need to find a solution for that case. #[inline(always)] -fn process_order(params: ExecuteOrderParams) { //TODO +fn process_order(params: ExecuteOrderParams) -> event_utils::LogData { + if (params.order.market.is_non_zero()) { + panic(array![OrderError::UNEXPECTED_MARKET]); + } + + validate_oracle_block_numbers( + params.min_oracle_block_numbers.span(), + params.max_oracle_block_numbers.span(), + params.order.order_type, + params.order.updated_at_block + ); + + let (output_token, output_amount) = swap_utils::swap( + @swap_utils::SwapParams { + data_store: params.contracts.data_store, + event_emitter: params.contracts.event_emitter, + oracle: params.contracts.oracle, + bank: IBankDispatcher { + contract_address: params.contracts.order_vault.contract_address + }, + key: params.key, + token_in: params.order.initial_collateral_token, + amount_in: params.order.initial_collateral_delta_amount, + swap_path_markets: params.swap_path_markets.span(), + min_output_amount: params.order.min_output_amount, + receiver: params.order.receiver, + ui_fee_receiver: params.order.ui_fee_receiver, + } + ); + + let mut address_items: event_utils::AddressItems = Default::default(); + let mut uint_items: event_utils::UintItems = Default::default(); + + address_items = + event_utils::set_item_address_items(address_items, 0, "output_token", output_token); + + uint_items = event_utils::set_item_uint_items(uint_items, 0, "output_amount", output_amount); + + event_utils::LogData { + address_items, + uint_items, + int_items: Default::default(), + bool_items: Default::default(), + felt252_items: Default::default(), + array_of_felt_items: Default::default(), + string_items: Default::default(), + } } @@ -20,9 +72,24 @@ fn process_order(params: ExecuteOrderParams) { //TODO /// * `order_updated_at_block` - the block at which the order was last updated. #[inline(always)] fn validate_oracle_block_numbers( - min_oracle_block_numbers: Array, - max_oracle_block_numbers: Array, + min_oracle_block_numbers: Span, + max_oracle_block_numbers: Span, order_type: OrderType, - order_updated_at_block: u128 -) { //TODO + order_updated_at_block: u64 +) { + if (order_type == OrderType::MarketSwap) { + oracle_utils::validate_block_number_within_range( + min_oracle_block_numbers, max_oracle_block_numbers, order_updated_at_block + ); + return; + } + if (order_type == OrderType::LimitSwap) { + if (!u64_are_gte(min_oracle_block_numbers, order_updated_at_block)) { + OracleError::ORACLE_BLOCK_NUMBERS_ARE_SMALLER_THAN_REQUIRED( + min_oracle_block_numbers, order_updated_at_block + ); + } + return; + } + panic(array![OrderError::UNSUPPORTED_ORDER_TYPE]); }