From b9698e43fed70cf0725df7988df721af23d161de Mon Sep 17 00:00:00 2001 From: zarboq <37303126+zarboq@users.noreply.github.com> Date: Tue, 3 Oct 2023 20:56:12 +0300 Subject: [PATCH 1/3] feat: implement order_vault & placeholder for test (#475) * feat: implement order_vault & placeholder for test * fix coding style --- src/bank/strict_bank.cairo | 12 ++++++++++++ src/order/order_vault.cairo | 22 ++++++++++++++++++---- tests/order/test_order_vault.cairo | 24 ++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 tests/order/test_order_vault.cairo diff --git a/src/bank/strict_bank.cairo b/src/bank/strict_bank.cairo index 18689369..fca76e38 100644 --- a/src/bank/strict_bank.cairo +++ b/src/bank/strict_bank.cairo @@ -40,6 +40,12 @@ trait IStrictBank { /// # Returns /// * The new balance. fn sync_token_balance(ref self: TContractState, token: ContractAddress) -> u128; + /// Records a token transfer into the contract. + /// # Arguments + /// * `token` - The token address to transfer. + /// # Returns + /// * The amount of tokens transferred. + fn record_transfer_in(ref self: TContractState, token: ContractAddress) -> u128; } #[starknet::contract] @@ -105,6 +111,12 @@ mod StrictBank { } fn sync_token_balance(ref self: ContractState, token: ContractAddress) -> u128 { + // TODO + 0 + } + + fn record_transfer_in(ref self: ContractState, token: ContractAddress) -> u128 { + // TODO 0 } } diff --git a/src/order/order_vault.cairo b/src/order/order_vault.cairo index 38a4fb2b..12c5046f 100644 --- a/src/order/order_vault.cairo +++ b/src/order/order_vault.cairo @@ -21,13 +21,20 @@ trait IOrderVault { fn transfer_out( ref self: TContractState, token: ContractAddress, receiver: ContractAddress, amount: u128, ); - /// Records a token transfer into the contract. /// # Arguments /// * `token` - The token address to transfer. /// # Returns /// * The amount of tokens transferred. fn record_transfer_in(ref self: TContractState, token: ContractAddress) -> u128; + /// Updates the `token_balances` in case of token burns or similar balance changes. + /// The `prev_balance` is not validated to be more than the `next_balance` as this + /// could allow someone to block this call by transferring into the contract. + /// # Arguments + /// * `token` - The token to record the burn for. + /// # Returns + /// * The new balance. + fn sync_token_balance(ref self: TContractState, token: ContractAddress) -> u128; } #[starknet::contract] @@ -77,12 +84,19 @@ mod OrderVault { token: ContractAddress, receiver: ContractAddress, amount: u128, - ) { // TODO + ) { + let mut state: StrictBank::ContractState = StrictBank::unsafe_new_contract_state(); + IStrictBank::transfer_out(ref state, token, receiver, amount); + } + + fn sync_token_balance(ref self: ContractState, token: ContractAddress) -> u128 { + let mut state: StrictBank::ContractState = StrictBank::unsafe_new_contract_state(); + IStrictBank::sync_token_balance(ref state, token) } fn record_transfer_in(ref self: ContractState, token: ContractAddress) -> u128 { - // TODO - 0 + let mut state: StrictBank::ContractState = StrictBank::unsafe_new_contract_state(); + IStrictBank::record_transfer_in(ref state, token) } } } diff --git a/tests/order/test_order_vault.cairo b/tests/order/test_order_vault.cairo new file mode 100644 index 00000000..685dccc1 --- /dev/null +++ b/tests/order/test_order_vault.cairo @@ -0,0 +1,24 @@ +// ************************************************************************* +// IMPORTS +// ************************************************************************* + +// Core lib imports. + +use result::ResultTrait; +use starknet::{ContractAddress, get_caller_address, contract_address_const, ClassHash}; +use snforge_std::{declare, ContractClassTrait, start_roll}; + +// TODO test when StrictBank functions will be implemented. + +// Local imports. +use satoru::utils::span32::{Span32, Array32Trait}; + +#[test] +fn given_normal_conditions_when_transfer_out_then_expect_balance_change() { // TODO +} + +/// Utility function to setup the test environment. +fn setup() -> (ContractAddress, IChainDispatcher,) {} + +/// Utility function to teardown the test environment. +fn teardown() {} From ca0e1a42b2ed6f6558dacd428e8d122f3eab8f73 Mon Sep 17 00:00:00 2001 From: dic0de <37063500+dic0de@users.noreply.github.com> Date: Thu, 5 Oct 2023 01:16:05 +0300 Subject: [PATCH 2/3] Adding Role admin check to the role-admin branch (#495) * Adding Role admin check to the role-admin branch * Precise panic error and code refactor --- src/role/error.cairo | 1 + src/role/role_store.cairo | 5 +++++ tests/role/test_role_store.cairo | 16 ++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/src/role/error.cairo b/src/role/error.cairo index 1b998415..3db56a47 100644 --- a/src/role/error.cairo +++ b/src/role/error.cairo @@ -1,3 +1,4 @@ mod RoleError { const UNAUTHORIZED_ACCESS: felt252 = 'unauthorized_access'; + const UNAUTHORIZED_CHANGE: felt252 = 'unauthorized_change'; } diff --git a/src/role/role_store.cairo b/src/role/role_store.cairo index eb78928a..f5e52c21 100644 --- a/src/role/role_store.cairo +++ b/src/role/role_store.cairo @@ -82,6 +82,7 @@ mod RoleStore { // Local imports. use satoru::role::{role, error::RoleError}; + // ************************************************************************* // STORAGE // ************************************************************************* @@ -164,6 +165,10 @@ mod RoleStore { fn revoke_role(ref self: ContractState, account: ContractAddress, role_key: felt252) { // Check that the caller has the admin role. self._assert_only_role(get_caller_address(), role::ROLE_ADMIN); + // check that the are more than 1 RoleAdmin + if role_key == role::ROLE_ADMIN { + assert(self.get_role_member_count(role_key) > 1, RoleError::UNAUTHORIZED_CHANGE); + } // Revoke the role. self._revoke_role(account, role_key); } diff --git a/tests/role/test_role_store.cairo b/tests/role/test_role_store.cairo index 735936e8..f144a143 100644 --- a/tests/role/test_role_store.cairo +++ b/tests/role/test_role_store.cairo @@ -42,6 +42,22 @@ fn given_normal_conditions_when_has_role_after_revoke_then_works() { // Check that the account address does not have the admin role. assert(!role_store.has_role(account_1(), ROLE_ADMIN), 'Invalid role'); } +#[test] +#[should_panic(expected: ('unauthorized_change',))] +fn given_normal_conditions_when_revoke_role_on_1_ROLE_ADMIN_panics() { + let role_store = setup(); + + // Use the address that has been used to deploy role_store. + start_prank(role_store.contract_address, admin()); + // assert that there is only one role ROLE_ADMIN present + assert(role_store.get_role_member_count(ROLE_ADMIN) == 1, 'members count != 1'); + + // Check that the account address has the admin role. + assert(role_store.has_role(admin(), ROLE_ADMIN), 'Invalid role'); + // Revoke role_admin should panic. + role_store.revoke_role(admin(), ROLE_ADMIN); +} + #[test] fn given_normal_conditions_when_get_role_count_then_works() { From 55c97b4ee954330369eda2267483afe440d2ddd3 Mon Sep 17 00:00:00 2001 From: zarboq <37303126+zarboq@users.noreply.github.com> Date: Thu, 5 Oct 2023 01:42:21 +0300 Subject: [PATCH 3/3] feat: implement decrease_position_collateral_utils (#465) * feat: implement decrease_position_collateral_utils * fixes: changes after review --------- Co-authored-by: Michel <105498726+Sk8erboi84@users.noreply.github.com> --- src/event/event_emitter.cairo | 27 +- src/lib.cairo | 1 + src/market/market_utils.cairo | 7 +- src/order/order.cairo | 3 +- .../decrease_position_collateral_utils.cairo | 917 +++++++++++++----- src/position/error.cairo | 14 +- src/position/position_utils.cairo | 22 +- src/pricing/position_pricing_utils.cairo | 16 +- src/utils/default.cairo | 10 + tests/data/test_position.cairo | 2 +- tests/event/test_market_events_emitted.cairo | 2 +- tests/market/test_market_utils.cairo | 15 +- ...t_decrease_position_collateral_utils.cairo | 240 +++++ tests/position/test_position_utils.cairo | 4 +- 14 files changed, 1012 insertions(+), 268 deletions(-) create mode 100644 src/utils/default.cairo create mode 100644 tests/position/test_decrease_position_collateral_utils.cairo diff --git a/src/event/event_emitter.cairo b/src/event/event_emitter.cairo index 72ef403a..1c629b08 100755 --- a/src/event/event_emitter.cairo +++ b/src/event/event_emitter.cairo @@ -9,17 +9,18 @@ use starknet::{ContractAddress, ClassHash}; // Local imports. use satoru::deposit::deposit::Deposit; use satoru::withdrawal::withdrawal::Withdrawal; -use satoru::position::position::Position; use satoru::market::market_pool_value_info::MarketPoolValueInfo; use satoru::pricing::swap_pricing_utils::SwapFees; -use satoru::position::position_event_utils::PositionIncreaseParams; -use satoru::position::position_utils::DecreasePositionCollateralValues; -use satoru::order::order::OrderType; +use satoru::position::{ + position::Position, position_event_utils::PositionIncreaseParams, + position_utils::DecreasePositionCollateralValues +}; use satoru::price::price::Price; use satoru::pricing::position_pricing_utils::PositionFees; -use satoru::order::order::{Order, SecondaryOrderType}; -use satoru::utils::span32::{Span32, DefaultSpan32}; -use satoru::utils::i128::{I128Div, I128Mul, I128Store, I128Serde}; +use satoru::order::order::{Order, SecondaryOrderType, OrderType}; +use satoru::utils::{ + i128::{I128Div, I128Mul, I128Store, I128Serde}, span32::{Span32, DefaultSpan32} +}; //TODO: OrderCollatDeltaAmountAutoUpdtd must be renamed back to OrderCollateralDeltaAmountAutoUpdated when string will be allowed as event argument @@ -57,7 +58,7 @@ trait IEventEmitter { /// Emits the `PositionImpactPoolAmountUpdated` event. fn emit_position_impact_pool_amount_updated( - ref self: TContractState, market: ContractAddress, delta: u128, next_value: u128, + ref self: TContractState, market: ContractAddress, delta: i128, next_value: u128, ); /// Emits the `SwapImpactPoolAmountUpdated` event. @@ -182,7 +183,7 @@ trait IEventEmitter { ref self: TContractState, order_key: felt252, position_collateral_amount: u128, - base_pnl_usd: u128, + base_pnl_usd: i128, remaining_cost_usd: u128 ); @@ -800,7 +801,7 @@ mod EventEmitter { #[derive(Drop, starknet::Event)] struct PositionImpactPoolAmountUpdated { market: ContractAddress, - delta: u128, + delta: i128, next_value: u128, } @@ -988,7 +989,7 @@ mod EventEmitter { struct InsolventClose { order_key: felt252, position_collateral_amount: u128, - base_pnl_usd: u128, + base_pnl_usd: i128, remaining_cost_usd: u128 } @@ -1607,7 +1608,7 @@ mod EventEmitter { /// Emits the `PositionImpactPoolAmountUpdated` event. fn emit_position_impact_pool_amount_updated( - ref self: ContractState, market: ContractAddress, delta: u128, next_value: u128, + ref self: ContractState, market: ContractAddress, delta: i128, next_value: u128, ) { self.emit(PositionImpactPoolAmountUpdated { market, delta, next_value, }); } @@ -1947,7 +1948,7 @@ mod EventEmitter { ref self: ContractState, order_key: felt252, position_collateral_amount: u128, - base_pnl_usd: u128, + base_pnl_usd: i128, remaining_cost_usd: u128 ) { self diff --git a/src/lib.cairo b/src/lib.cairo index 51e05841..8b11aa79 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -152,6 +152,7 @@ mod utils { mod error_utils; mod starknet_utils; mod traits; + mod default; } // `liquidation` function to help with liquidations. diff --git a/src/market/market_utils.cairo b/src/market/market_utils.cairo index 5d507f8c..61fe6d37 100644 --- a/src/market/market_utils.cairo +++ b/src/market/market_utils.cairo @@ -497,7 +497,6 @@ fn get_max_open_interest( /// * `delta` - The amount to increment by. fn increment_claimable_collateral_amount( data_store: IDataStoreDispatcher, - chain: IChainDispatcher, event_emitter: IEventEmitterDispatcher, market_address: ContractAddress, token: ContractAddress, @@ -507,7 +506,7 @@ fn increment_claimable_collateral_amount( let divisor = data_store.get_u128(keys::claimable_collateral_time_divisor()); error_utils::check_division_by_zero(divisor, 'increment_claimable_collateral'); // Get current timestamp. - let current_timestamp = chain.get_block_timestamp().into(); + let current_timestamp = get_block_timestamp().into(); let time_key = current_timestamp / divisor; // Increment the collateral amount for the account. @@ -870,11 +869,11 @@ fn apply_delta_to_position_impact_pool( data_store: IDataStoreDispatcher, event_emitter: IEventEmitterDispatcher, market_address: ContractAddress, - delta: u128 + delta: i128 ) -> u128 { // Increment the position impact pool amount. let next_value = data_store - .increment_u128(keys::position_impact_pool_amount_key(market_address), delta); + .apply_bounded_delta_to_u128(keys::position_impact_pool_amount_key(market_address), delta); // Emit event. event_emitter.emit_position_impact_pool_amount_updated(market_address, delta, next_value); diff --git a/src/order/order.cairo b/src/order/order.cairo index ba889e44..7d27c642 100644 --- a/src/order/order.cairo +++ b/src/order/order.cairo @@ -119,8 +119,9 @@ enum OrderType { } /// To help further differentiate orders. -#[derive(Drop, Copy, starknet::Store, Serde)] +#[derive(Drop, starknet::Store, Serde, PartialEq, Copy, Default)] enum SecondaryOrderType { + #[default] None, Adl, } diff --git a/src/position/decrease_position_collateral_utils.cairo b/src/position/decrease_position_collateral_utils.cairo index dbb8450b..45d579cb 100644 --- a/src/position/decrease_position_collateral_utils.cairo +++ b/src/position/decrease_position_collateral_utils.cairo @@ -6,20 +6,19 @@ // Core lib imports. use starknet::{ContractAddress, contract_address_const}; use result::ResultTrait; - // Local imports. -use satoru::position::position_utils::{ - DecreasePositionCollateralValues, UpdatePositionParams, DecreasePositionCache, - DecreasePositionCollateralValuesOutput -}; -use satoru::pricing::position_pricing_utils::{ - PositionFees, PositionBorrowingFees, PositionFundingFees, PositionReferralFees, PositionUiFees, -}; -use satoru::market::market_utils::MarketPrices; -use satoru::price::price::Price; +use satoru::position::{position_utils, decrease_position_swap_utils, error}; +use satoru::pricing::position_pricing_utils; +use satoru::market::market_utils; +use satoru::price::price::{Price, PriceTrait}; +use satoru::order::{base_order_utils, order}; +use satoru::utils::{i128::{I128Serde, I128Default, I128Store}, calc, precision}; +use satoru::data::{keys, data_store::{IDataStoreDispatcher, IDataStoreDispatcherTrait}}; +use satoru::event::event_emitter::{IEventEmitterDispatcher, IEventEmitterDispatcherTrait}; +use satoru::fee::fee_utils; /// Struct used in process_collateral function as cache. -#[derive(Drop, starknet::Store, Serde)] +#[derive(Drop, starknet::Store, Serde, Default, Copy)] struct ProcessCollateralCache { /// Wether an insolvent close is allowed or not. is_insolvent_close_allowed: bool, @@ -32,7 +31,7 @@ struct ProcessCollateralCache { } /// Struct to store pay_for_cost function returned result. -#[derive(Drop, starknet::Store, Serde)] +#[derive(Drop, starknet::Store, Serde, Default, Copy)] struct PayForCostResult { /// The amount of collateral token paid as cost. amount_paid_in_collateral_token: u128, @@ -43,86 +42,492 @@ struct PayForCostResult { } /// Struct used in get_execution_price function as cache. -#[derive(Drop, starknet::Store, Serde)] +#[derive(Drop, starknet::Store, Serde, Default)] struct GetExecutionPriceCache { /// The price impact induced by execution. - price_impact_usd: u128, // TODO replace with i128 when it derives Store + price_impact_usd: i128, /// The difference between maximum price impact and originally calculated price impact. - priceImpactDiffUsd: u128, + price_impact_diff_usd: u128, /// The execution price. execution_price: u128, } /// Handle the collateral changes of the position. /// # Returns -/// (DecreasePositionCollateralValues, PositionFees) +/// The values linked to the process of a decrease of collateral and position fees. #[inline(always)] fn process_collateral( - params: UpdatePositionParams, cache: DecreasePositionCache -) -> (DecreasePositionCollateralValues, PositionFees) { - // TODO - let address_zero = contract_address_const::<0>(); - let decrease_position_collateral_values_output = DecreasePositionCollateralValuesOutput { - output_token: address_zero, - output_amount: 0, - secondary_output_token: address_zero, - secondary_output_amount: 0, - }; - let decrease_position_collateral_values = DecreasePositionCollateralValues { - execution_price: 0, - remaining_collateral_amount: 0, - base_pnl_usd: 0, - uncapped_base_pnl_usd: 0, - size_delta_in_tokens: 0, - price_impact_usd: 0, - price_impact_diff_usd: 0, - output: decrease_position_collateral_values_output - }; - let position_referral_fees = PositionReferralFees { - referral_code: 0, - affiliate: address_zero, - trader: address_zero, - total_rebate_factor: 0, - trader_discount_factor: 0, - total_rebate_amount: 0, - trader_discount_amount: 0, - affiliate_reward_amount: 0, - }; - let position_funding_fees = PositionFundingFees { - funding_fee_amount: 0, - claimable_long_token_amount: 0, - claimable_short_token_amount: 0, - latest_funding_fee_amount_per_size: 0, - latest_long_token_claimable_funding_amount_per_size: 0, - latest_short_token_claimable_funding_amount_per_size: 0, - }; - let position_borrowing_fees = PositionBorrowingFees { - borrowing_fee_usd: 0, - borrowing_fee_amount: 0, - borrowing_fee_receiver_factor: 0, - borrowing_fee_amount_for_fee_receiver: 0, - }; - let position_ui_fees = PositionUiFees { - ui_fee_receiver: address_zero, ui_fee_receiver_factor: 0, ui_fee_amount: 0, + mut params: position_utils::UpdatePositionParams, cache: position_utils::DecreasePositionCache +) -> (position_utils::DecreasePositionCollateralValues, position_pricing_utils::PositionFees) { + let mut collateral_cache: ProcessCollateralCache = Default::default(); + let mut values: position_utils::DecreasePositionCollateralValues = Default::default(); + values.output.output_token = params.position.collateral_token; + values.output.secondary_output_token = cache.pnl_token; + + // only allow insolvent closing if it is a liquidation or ADL order + // is_insolvent_close_allowed is used in handleEarlyReturn to determine + // whether the txn should revert if the remainingCostUsd is below zero + // + // for is_insolvent_close_allowed to be true, the size_delta_usd must equal + // the position size, otherwise there may be pending positive pnl that + // could be used to pay for fees and the position would be undercharged + // if the position is not fully closed + // + // for ADLs it may be possible that a position needs to be closed by a larger + // size to fully pay for fees, but closing by that larger size could cause a PnlOvercorrected + // error to be thrown in AdlHandler, this case should be rare + collateral_cache + .is_insolvent_close_allowed = params + .order + .size_delta_usd == params + .position + .size_in_usd + && (base_order_utils::is_liquidation_order(params.order.order_type) + || params.secondary_order_type == order::SecondaryOrderType::Adl(())); + // in case price impact is too high it is capped and the difference is made to be claimable + // the execution price is based on the capped price impact so it may be a better price than what it should be + // price_impact_diff_usd is the difference between the maximum price impact and the originally calculated price impact + // e.g. if the originally calculated price impact is -$100, but the capped price impact is -$80 + // then priceImpactDiffUsd would be $20 + let (price_impact_usd_, price_impact_diff_usd_, execution_price_) = get_execution_price( + params, cache.prices.index_token_price + ); + values.price_impact_usd = price_impact_usd_; + values.price_impact_diff_usd = price_impact_diff_usd_; + values.execution_price = execution_price_; + // the total_position_pnl is calculated based on the current indexTokenPrice instead of the executionPrice + // since the executionPrice factors in price impact which should be accounted for separately + // the sizeDeltaInTokens is calculated as position.size_in_tokens() * size_delta_usd / position.size_in_usd() + // the basePnlUsd is the pnl to be realized, and is calculated as: + // total_position_pnl * size_delta_in_tokens / position.size_in_tokens() + let (base_pnl_usd_, uncapped_base_pnl_usd_, size_delta_in_tokens_) = + position_utils::get_position_pnl_usd( + params.contracts.data_store, + params.market, + cache.prices, + params.position, + params.order.size_delta_usd + ); + values.base_pnl_usd = base_pnl_usd_; + values.uncapped_base_pnl_usd = uncapped_base_pnl_usd_; + values.size_delta_in_tokens = size_delta_in_tokens_; + + let get_position_fees_params: position_pricing_utils::GetPositionFeesParams = + position_pricing_utils::GetPositionFeesParams { + data_store: params.contracts.data_store, + referral_storage: params.contracts.referral_storage, + position: params.position, + collateral_token_price: cache.collateral_token_price, + for_positive_impact: values.price_impact_usd > 0, + long_token: params.market.long_token, + short_token: params.market.short_token, + size_delta_usd: params.order.size_delta_usd, + ui_fee_receiver: params.order.ui_fee_receiver, }; - let price = Price { min: 0, max: 0, }; - let position_fees = PositionFees { - referral: position_referral_fees, - funding: position_funding_fees, - borrowing: position_borrowing_fees, - ui: position_ui_fees, - collateral_token_price: price, - position_fee_factor: 0, - protocol_fee_amount: 0, - position_fee_receiver_factor: 0, - fee_receiver_amount: 0, - fee_amount_for_pool: 0, - position_fee_amount_for_pool: 0, - position_fee_amount: 0, - total_cost_amount_excluding_funding: 0, - total_cost_amount: 0, + + let mut fees: position_pricing_utils::PositionFees = position_pricing_utils::get_position_fees( + get_position_fees_params + ); + + // if the pnl is positive, deduct the pnl amount from the pool + if values.base_pnl_usd > 0 { + // use pnl_token_price.max to minimize the tokens paid out + let deduction_amount_for_pool: u128 = calc::to_unsigned(values.base_pnl_usd) + / cache.pnl_token_price.max; + + market_utils::apply_delta_to_pool_amount( + params.contracts.data_store, + params.contracts.event_emitter, + params.market, + cache.pnl_token, + calc::to_signed(deduction_amount_for_pool, false) + ); + + if values.output.output_token == cache.pnl_token { + values.output.output_amount += deduction_amount_for_pool; + } else { + values.output.secondary_output_amount += deduction_amount_for_pool; + } + } + + if values.price_impact_usd > 0 { + // use indexTokenPrice.min to maximize the position impact pool reduction + let deduction_amount_for_impact_pool = calc::roundup_division( + calc::to_unsigned(values.price_impact_usd), cache.prices.index_token_price.min + ); + + market_utils::apply_delta_to_position_impact_pool( + params.contracts.data_store, + params.contracts.event_emitter, + params.market.market_token, + calc::to_signed(deduction_amount_for_impact_pool, false) + ); + + // use pnlTokenPrice.max to minimize the payout from the pool + // some impact pool value may be transferred to the market token pool if there is a + // large spread between min and max prices + // since if there is a positive priceImpactUsd, the impact pool would be reduced using indexTokenPrice.min to + // maximize the deduction value, while the market token pool is reduced using the pnlTokenPrice.max to minimize + // the deduction value + // the pool value is calculated by subtracting the worth of the tokens in the position impact pool + // so this transfer of value would increase the price of the market token + let deduction_amount_for_pool: u128 = calc::to_unsigned(values.price_impact_usd) + / cache.pnl_token_price.max; + + market_utils::apply_delta_to_pool_amount( + params.contracts.data_store, + params.contracts.event_emitter, + params.market, + cache.pnl_token, + calc::to_signed(deduction_amount_for_pool, false) + ); + + if values.output.output_token == cache.pnl_token { + values.output.output_amount += deduction_amount_for_pool; + } else { + values.output.secondary_output_amount += deduction_amount_for_pool; + } + } + + // swap profit to the collateral token + // if the decreasePositionSwapType was set to NoSwap or if the swap fails due + // to insufficient liquidity or other reasons then it is possible that + // the profit remains in a different token from the collateral token + let (was_swapped_, swap_output_amount_) = + decrease_position_swap_utils::swap_profit_to_collateral_token( + params, cache.pnl_token, values.output.secondary_output_amount + ); + collateral_cache.was_swapped = was_swapped_; + collateral_cache.swap_output_amount = swap_output_amount_; + + // if the swap was successful the profit should have been swapped + // to the collateral token + if collateral_cache.was_swapped { + values.output.output_amount += collateral_cache.swap_output_amount; + values.output.secondary_output_amount = 0; + } + + values.remaining_collateral_amount = params.position.collateral_amount; + + // pay for funding fees + let (values_, result_) = pay_for_cost( + params, + values, + cache.prices, + cache.collateral_token_price, + // use collateralTokenPrice.min because the payForCost + // will divide the USD value by the price.min as well + fees.funding.funding_fee_amount * cache.collateral_token_price.min + ); + values = values_; + collateral_cache.result = result_; + if collateral_cache.result.amount_paid_in_secondary_output_token > 0 { + let holding_address: ContractAddress = params + .contracts + .data_store + .get_address(keys::holding_address()); + + if holding_address.is_zero() { + panic_with_felt252(error::PositionError::EMPTY_HOLDING_ADDRESS); + } + + // send the funding fee amount to the holding address + // this funding fee amount should be swapped to the required token + // and the resulting tokens should be deposited back into the pool + market_utils::increment_claimable_collateral_amount( + params.contracts.data_store, + params.contracts.event_emitter, + params.market.market_token, + values.output.secondary_output_token, + holding_address, + collateral_cache.result.amount_paid_in_secondary_output_token + ); + } + + if collateral_cache.result.amount_paid_in_collateral_token < fees.funding.funding_fee_amount { + // the case where this is insufficient collateral to pay funding fees + // should be rare, and the difference should be small + // in case it happens, the pool should be topped up with the required amount using + // the claimable amount sent to the holding address, an insurance fund, or similar mechanism + params + .contracts + .event_emitter + .emit_insufficient_funding_fee_payment( + params.market.market_token, + params.position.collateral_token, + fees.funding.funding_fee_amount, + collateral_cache.result.amount_paid_in_collateral_token, + collateral_cache.result.amount_paid_in_secondary_output_token + ); + } + + if collateral_cache.result.remaining_cost_usd > 0 { + return handle_early_return(params, @values, fees, collateral_cache, 'funding'); }; - (decrease_position_collateral_values, position_fees) + + // pay for negative pnl + if values.base_pnl_usd < 0 { + let (values_, result_) = pay_for_cost( + params, + values, + cache.prices, + cache.collateral_token_price, + calc::to_unsigned(-values.base_pnl_usd) + ); + values = values_; + collateral_cache.result = result_; + + if collateral_cache.result.amount_paid_in_collateral_token > 0 { + market_utils::apply_delta_to_pool_amount( + params.contracts.data_store, + params.contracts.event_emitter, + params.market, + params.position.collateral_token, + calc::to_signed(collateral_cache.result.amount_paid_in_collateral_token, true) + ); + } + + if collateral_cache.result.amount_paid_in_secondary_output_token > 0 { + market_utils::apply_delta_to_pool_amount( + params.contracts.data_store, + params.contracts.event_emitter, + params.market, + values.output.secondary_output_token, + calc::to_signed(collateral_cache.result.amount_paid_in_secondary_output_token, true) + ); + } + + if collateral_cache.result.remaining_cost_usd > 0 { + return handle_early_return(params, @values, fees, collateral_cache, 'pnl'); + } + } + + // pay for fees + let (values_, result_) = pay_for_cost( + params, + values, + cache.prices, + cache.collateral_token_price, + // use collateral_token_price.min because the pay_for_cost + // will divide the USD value by the price.min as well + fees.total_cost_amount_excluding_funding * cache.collateral_token_price.min + ); + values = values_; + collateral_cache.result = result_; + + // if fees were fully paid in the collateral token, update the pool and claimable fee amounts + if collateral_cache.result.remaining_cost_usd == 0 + && collateral_cache.result.amount_paid_in_secondary_output_token == 0 { + // there may be a large amount of borrowing fees that could have been accumulated + // these fees could cause the pool to become unbalanced, price impact is not paid for causing + // this imbalance + // the swap impact pool should be built up so that it can be used to pay for positive price impact + // for re-balancing to help handle this case + market_utils::apply_delta_to_pool_amount( + params.contracts.data_store, + params.contracts.event_emitter, + params.market, + params.position.collateral_token, + calc::to_signed(fees.fee_amount_for_pool, true) + ); + + fee_utils::increment_claimable_fee_amount( + params.contracts.data_store, + params.contracts.event_emitter, + params.market.market_token, + params.position.collateral_token, + fees.fee_receiver_amount, + keys::position_fee_type() + ); + + fee_utils::increment_claimable_ui_fee_amount( + params.contracts.data_store, + params.contracts.event_emitter, + params.order.ui_fee_receiver, + params.market.market_token, + params.position.collateral_token, + fees.ui.ui_fee_amount, + keys::ui_position_fee_type() + ); + } else { + // the fees are expected to be paid in the collateral token + // if there are insufficient funds to pay for fees entirely in the collateral token + // then credit the fee amount entirely to the pool + if collateral_cache.result.amount_paid_in_collateral_token > 0 { + market_utils::apply_delta_to_pool_amount( + params.contracts.data_store, + params.contracts.event_emitter, + params.market, + params.position.collateral_token, + calc::to_signed(collateral_cache.result.amount_paid_in_collateral_token, true) + ); + } + + if collateral_cache.result.amount_paid_in_secondary_output_token > 0 { + market_utils::apply_delta_to_pool_amount( + params.contracts.data_store, + params.contracts.event_emitter, + params.market, + values.output.secondary_output_token, + calc::to_signed(collateral_cache.result.amount_paid_in_secondary_output_token, true) + ); + } + + // empty the fees since the amount was entirely paid to the pool instead of for fees + // it is possible for the txn execution to still complete even in this case + // as long as the remainingCostUsd is still zero + fees = get_empty_fees(@fees); + } + + if collateral_cache.result.remaining_cost_usd > 0 { + return handle_early_return(params, @values, fees, collateral_cache, 'fees'); + } + + // pay for negative price impact + if values.price_impact_usd < 0 { + let (values_, result_) = pay_for_cost( + params, + values, + cache.prices, + cache.collateral_token_price, + calc::to_unsigned(-values.price_impact_usd) + ); + values = values_; + collateral_cache.result = result_; + + if collateral_cache.result.amount_paid_in_collateral_token > 0 { + market_utils::apply_delta_to_pool_amount( + params.contracts.data_store, + params.contracts.event_emitter, + params.market, + params.position.collateral_token, + calc::to_signed(collateral_cache.result.amount_paid_in_collateral_token, true) + ); + + market_utils::apply_delta_to_position_impact_pool( + params.contracts.data_store, + params.contracts.event_emitter, + params.market.market_token, + calc::to_signed( + collateral_cache.result.amount_paid_in_collateral_token + * cache.collateral_token_price.min + / cache.prices.index_token_price.max, + true + ) + ); + } + + if collateral_cache.result.amount_paid_in_secondary_output_token > 0 { + market_utils::apply_delta_to_pool_amount( + params.contracts.data_store, + params.contracts.event_emitter, + params.market, + values.output.secondary_output_token, + calc::to_signed(collateral_cache.result.amount_paid_in_secondary_output_token, true) + ); + + market_utils::apply_delta_to_position_impact_pool( + params.contracts.data_store, + params.contracts.event_emitter, + params.market.market_token, + calc::to_signed( + collateral_cache.result.amount_paid_in_secondary_output_token + * cache.pnl_token_price.min + / cache.prices.index_token_price.max, + true + ) + ); + } + + if collateral_cache.result.remaining_cost_usd > 0 { + return handle_early_return(params, @values, fees, collateral_cache, 'impact'); + } + } + + // pay for price impact diff + if values.price_impact_diff_usd > 0 { + let (values_, result_) = pay_for_cost( + params, values, cache.prices, cache.collateral_token_price, values.price_impact_diff_usd + ); + values = values_; + collateral_cache.result = result_; + + if collateral_cache.result.amount_paid_in_collateral_token > 0 { + market_utils::increment_claimable_collateral_amount( + params.contracts.data_store, + params.contracts.event_emitter, + params.market.market_token, + params.position.collateral_token, + params.order.account, + collateral_cache.result.amount_paid_in_collateral_token + ); + } + + if collateral_cache.result.amount_paid_in_secondary_output_token > 0 { + market_utils::increment_claimable_collateral_amount( + params.contracts.data_store, + params.contracts.event_emitter, + params.market.market_token, + values.output.secondary_output_token, + params.order.account, + collateral_cache.result.amount_paid_in_secondary_output_token + ); + } + + if collateral_cache.result.remaining_cost_usd > 0 { + return handle_early_return(params, @values, fees, collateral_cache, 'diff'); + } + } + + // the priceImpactDiffUsd has been deducted from the output amount or the position's collateral + // to reduce the chance that the position's collateral is reduced by an unexpected amount, adjust the + // initialCollateralDeltaAmount by the priceImpactDiffAmount + // this would also help to prevent the position's leverage from being unexpectedly increased + // + // note that this calculation may not be entirely accurate since it is possible that the priceImpactDiffUsd + // could have been paid with one of or a combination of collateral / outputAmount / secondaryOutputAmount + if params.order.initial_collateral_delta_amount > 0 && values.price_impact_diff_usd > 0 { + let initial_collateral_delta_amount: u128 = params.order.initial_collateral_delta_amount; + + let price_impact_diff_amount: u128 = values.price_impact_diff_usd + / cache.collateral_token_price.min; + if initial_collateral_delta_amount > price_impact_diff_amount { + params.order.initial_collateral_delta_amount = initial_collateral_delta_amount + - price_impact_diff_amount; + } else { + params.order.initial_collateral_delta_amount = 0; + } + + params + .contracts + .event_emitter + .emit_order_collateral_delta_amount_auto_updated( + params.order_key, + initial_collateral_delta_amount, // collateral_delta_amount + params.order.initial_collateral_delta_amount // next_collateral_delta_amount + ); + } + + // cap the withdrawable amount to the remainingCollateralAmount + if params.order.initial_collateral_delta_amount > values.remaining_collateral_amount { + params + .contracts + .event_emitter + .emit_order_collateral_delta_amount_auto_updated( + params.order_key, + params.order.initial_collateral_delta_amount, // collateral_delta_amount + values.remaining_collateral_amount // next_collateral_delta_amount + ); + + params.order.initial_collateral_delta_amount = values.remaining_collateral_amount; + } + + if params.order.initial_collateral_delta_amount > 0 { + values.remaining_collateral_amount -= params.order.initial_collateral_delta_amount; + values.output.output_amount += params.order.initial_collateral_delta_amount; + } + + (values, fees) } /// Compute execution price of the position update. @@ -131,10 +536,85 @@ fn process_collateral( /// * `index_token_price` - The price of the index token. /// (price_impact_usd, price_impact_diff_usd, execution_price) fn get_execution_price( - params: UpdatePositionParams, index_token_price: Price + params: position_utils::UpdatePositionParams, index_token_price: Price ) -> (i128, u128, u128) { - // TODO - (0, 0, 0) + let size_delta_usd: u128 = params.order.size_delta_usd; + + // note that the executionPrice is not validated against the order.acceptable_price value + // if the size_delta_usd is zero + // for limit orders the order.triggerPrice should still have been validated + if size_delta_usd == 0 { + // decrease order: + // - long: use the smaller price + // - short: use the larger price + return (0, 0, index_token_price.pick_price(!params.position.is_long)); + } + + let mut cache: GetExecutionPriceCache = Default::default(); + + cache + .price_impact_usd = + position_pricing_utils::get_price_impact_usd( + position_pricing_utils::GetPriceImpactUsdParams { + data_store: params.contracts.data_store, + market: params.market, + usd_delta: calc::to_signed(size_delta_usd, false), + is_long: params.order.is_long, + } + ); + + // cap priceImpactUsd based on the amount available in the position impact pool + cache + .price_impact_usd = + market_utils::get_capped_position_impact_usd( + params.contracts.data_store, + params.market.market_token, + index_token_price, + cache.price_impact_usd, + size_delta_usd + ); + + if cache.price_impact_usd < 0 { + let max_price_impact_factor: u128 = market_utils::get_max_position_impact_factor( + params.contracts.data_store, params.market.market_token, false + ); + + // convert the max price impact to the min negative value + // e.g. if size_delta_usd is 10,000 and max_price_impact_factor is 2% + // then minPriceImpactUsd = -200 + let min_price_impact_usd: i128 = calc::to_signed( + precision::apply_factor_u128(size_delta_usd, max_price_impact_factor), false + ); + + // cap priceImpactUsd to the min negative value and store the difference in price_impact_diff_usd + // e.g. if price_impact_usd is -500 and min_price_impact_usd is -200 + // then set price_impact_diff_usd to -200 - -500 = 300 + // set priceImpactUsd to -200 + if cache.price_impact_usd < min_price_impact_usd { + cache + .price_impact_diff_usd = + calc::to_unsigned(min_price_impact_usd - cache.price_impact_usd); + cache.price_impact_usd = min_price_impact_usd; + } + } + + // the execution_price is calculated after the price impact is capped + // so the output amount directly received by the user may not match + // the execution_price, the difference would be in the stored as a + // claimable amount + cache + .execution_price = + base_order_utils::get_execution_price_for_decrease( + index_token_price, + params.position.size_in_usd, + params.position.size_in_tokens, + size_delta_usd, + cache.price_impact_usd, + params.order.acceptable_price, + params.position.is_long + ); + + (cache.price_impact_usd, cache.price_impact_diff_usd, cache.execution_price) } /// Pay costs of the position update. @@ -145,38 +625,81 @@ fn get_execution_price( /// * `collateral_token_price` - The prices of the collateral token. /// * `cost_usd` - The total cost in usd. /// # Returns -/// Updated DecreasePositionCollateralValues and output of pay for cost. +/// Updated position_utils::DecreasePositionCollateralValues and output of pay for cost. fn pay_for_cost( - params: UpdatePositionParams, - values: DecreasePositionCollateralValues, - prices: MarketPrices, + params: position_utils::UpdatePositionParams, + mut values: position_utils::DecreasePositionCollateralValues, + prices: market_utils::MarketPrices, collateral_token_price: Price, cost_usd: u128, -) -> (DecreasePositionCollateralValues, PayForCostResult) { - // TODO - let address_zero = contract_address_const::<0>(); - let decrease_position_collateral_values_output = DecreasePositionCollateralValuesOutput { - output_token: address_zero, - output_amount: 0, - secondary_output_token: address_zero, - secondary_output_amount: 0, - }; - let decrease_position_collateral_values = DecreasePositionCollateralValues { - execution_price: 0, - remaining_collateral_amount: 0, - base_pnl_usd: 0, - uncapped_base_pnl_usd: 0, - size_delta_in_tokens: 0, - price_impact_usd: 0, - price_impact_diff_usd: 0, - output: decrease_position_collateral_values_output - }; - let pay_for_cost_result = PayForCostResult { - amount_paid_in_collateral_token: 0, - amount_paid_in_secondary_output_token: 0, - remaining_cost_usd: 0, - }; - (decrease_position_collateral_values, pay_for_cost_result) +) -> (position_utils::DecreasePositionCollateralValues, PayForCostResult) { + let mut result: PayForCostResult = Default::default(); + + if cost_usd == 0 { + return (values, result); + } + + let mut remaining_cost_in_output_token: u128 = calc::roundup_division( + cost_usd, collateral_token_price.min + ); + + if values.output.output_amount > 0 { + if values.output.output_amount > remaining_cost_in_output_token { + result.amount_paid_in_collateral_token += remaining_cost_in_output_token; + values.output.output_amount -= remaining_cost_in_output_token; + remaining_cost_in_output_token = 0; + } else { + result.amount_paid_in_collateral_token += values.output.output_amount; + remaining_cost_in_output_token -= values.output.output_amount; + values.output.output_amount = 0; + } + } + + if remaining_cost_in_output_token == 0 { + return (values, result); + } + + if (values.remaining_collateral_amount > 0) { + if (values.remaining_collateral_amount > remaining_cost_in_output_token) { + result.amount_paid_in_collateral_token += remaining_cost_in_output_token; + values.remaining_collateral_amount -= remaining_cost_in_output_token; + remaining_cost_in_output_token = 0; + } else { + result.amount_paid_in_collateral_token += values.remaining_collateral_amount; + remaining_cost_in_output_token -= values.remaining_collateral_amount; + values.remaining_collateral_amount = 0; + } + } + + if remaining_cost_in_output_token == 0 { + return (values, result); + } + + let secondary_output_token_price: Price = market_utils::get_cached_token_price( + values.output.secondary_output_token, params.market, prices + ); + + let mut remaining_cost_in_secondary_output_token: u128 = remaining_cost_in_output_token + * collateral_token_price.min + / secondary_output_token_price.min; + + if (values.output.secondary_output_amount > 0) { + if (values.output.secondary_output_amount > remaining_cost_in_secondary_output_token) { + result + .amount_paid_in_secondary_output_token += remaining_cost_in_secondary_output_token; + values.output.secondary_output_amount -= remaining_cost_in_secondary_output_token; + remaining_cost_in_secondary_output_token = 0; + } else { + result.amount_paid_in_secondary_output_token += values.output.secondary_output_amount; + remaining_cost_in_secondary_output_token -= values.output.secondary_output_amount; + values.output.secondary_output_amount = 0; + } + } + + result.remaining_cost_usd = remaining_cost_in_secondary_output_token + * secondary_output_token_price.min; + + (values, result) } /// Handle early return case where there is still remaining costs. @@ -186,121 +709,83 @@ fn pay_for_cost( /// * `fees` - The position fees. /// * `collateral_cache` - The struct used as cache in process_collateral. /// # Returns -/// Updated DecreasePositionCollateralValues and position fees. +/// Updated position_utils::DecreasePositionCollateralValues and position fees. fn handle_early_return( - params: UpdatePositionParams, - values: DecreasePositionCollateralValues, - fees: PositionFees, + params: position_utils::UpdatePositionParams, + values: @position_utils::DecreasePositionCollateralValues, + fees: position_pricing_utils::PositionFees, collateral_cache: ProcessCollateralCache, -) -> (DecreasePositionCollateralValues, PositionFees) { - // TODO - let address_zero = contract_address_const::<0>(); - let decrease_position_collateral_values_output = DecreasePositionCollateralValuesOutput { - output_token: address_zero, - output_amount: 0, - secondary_output_token: address_zero, - secondary_output_amount: 0, - }; - let decrease_position_collateral_values = DecreasePositionCollateralValues { - execution_price: 0, - remaining_collateral_amount: 0, - base_pnl_usd: 0, - uncapped_base_pnl_usd: 0, - size_delta_in_tokens: 0, - price_impact_usd: 0, - price_impact_diff_usd: 0, - output: decrease_position_collateral_values_output - }; - let position_referral_fees = PositionReferralFees { - referral_code: 0, - affiliate: address_zero, - trader: address_zero, - total_rebate_factor: 0, - trader_discount_factor: 0, - total_rebate_amount: 0, - trader_discount_amount: 0, - affiliate_reward_amount: 0, - }; - let position_funding_fees = PositionFundingFees { - funding_fee_amount: 0, - claimable_long_token_amount: 0, - claimable_short_token_amount: 0, - latest_funding_fee_amount_per_size: 0, - latest_long_token_claimable_funding_amount_per_size: 0, - latest_short_token_claimable_funding_amount_per_size: 0, - }; - let position_borrowing_fees = PositionBorrowingFees { - borrowing_fee_usd: 0, - borrowing_fee_amount: 0, - borrowing_fee_receiver_factor: 0, - borrowing_fee_amount_for_fee_receiver: 0, - }; - let position_ui_fees = PositionUiFees { - ui_fee_receiver: address_zero, ui_fee_receiver_factor: 0, ui_fee_amount: 0, - }; - let price = Price { min: 0, max: 0, }; - let position_fees = PositionFees { - referral: position_referral_fees, - funding: position_funding_fees, - borrowing: position_borrowing_fees, - ui: position_ui_fees, - collateral_token_price: price, - position_fee_factor: 0, - protocol_fee_amount: 0, - position_fee_receiver_factor: 0, - fee_receiver_amount: 0, - fee_amount_for_pool: 0, - position_fee_amount_for_pool: 0, - position_fee_amount: 0, - total_cost_amount_excluding_funding: 0, - total_cost_amount: 0, - }; - (decrease_position_collateral_values, position_fees) + step: felt252 +) -> (position_utils::DecreasePositionCollateralValues, position_pricing_utils::PositionFees) { + if (!collateral_cache.is_insolvent_close_allowed) { + error::PositionError::INSUFFICIENT_FUNDS_TO_PAY_FOR_COSTS( + collateral_cache.result.remaining_cost_usd, step + ); + } + + params + .contracts + .event_emitter + .emit_position_fees_info( + params.order_key, + params.position_key, + params.market.market_token, + params.position.collateral_token, + params.order.size_delta_usd, + false, // isIncrease + fees + ); + + params + .contracts + .event_emitter + .emit_insolvent_close_info( + params.order_key, + params.position.collateral_amount, + *values.base_pnl_usd, + collateral_cache.result.remaining_cost_usd + ); + + (*values, get_empty_fees(@fees)) } /// Return empty fees struct using fees struct given in parameter. /// Keep useful values such as accumulated funding fees. /// # Arguments -/// * `fees` - The PositionFees struct used to get the new empty struct. +/// * `fees` - The position_pricing_utils::PositionFees struct used to get the new empty struct. /// # Returns -/// An empty PositionFees struct. -fn get_empty_fees(fees: PositionFees) -> PositionFees { - // TODO - let address_zero = contract_address_const::<0>(); - let position_referral_fees = PositionReferralFees { - referral_code: 0, - affiliate: address_zero, - trader: address_zero, - total_rebate_factor: 0, - trader_discount_factor: 0, - total_rebate_amount: 0, - trader_discount_amount: 0, - affiliate_reward_amount: 0, - }; - let position_funding_fees = PositionFundingFees { +/// An empty position_pricing_utils::PositionFees struct. +fn get_empty_fees( + fees: @position_pricing_utils::PositionFees +) -> position_pricing_utils::PositionFees { + let referral: position_pricing_utils::PositionReferralFees = Default::default(); + + // allow the accumulated funding fees to still be claimable + // return the latestFundingFeeAmountPerSize, latest_long_token_claimable_funding_amount_per_size, + // latest_short_token_claimable_funding_amount_per_size values as these may be used to update the + // position's values if the position will be partially closed + let funding = position_pricing_utils::PositionFundingFees { funding_fee_amount: 0, - claimable_long_token_amount: 0, - claimable_short_token_amount: 0, - latest_funding_fee_amount_per_size: 0, - latest_long_token_claimable_funding_amount_per_size: 0, - latest_short_token_claimable_funding_amount_per_size: 0, - }; - let position_borrowing_fees = PositionBorrowingFees { - borrowing_fee_usd: 0, - borrowing_fee_amount: 0, - borrowing_fee_receiver_factor: 0, - borrowing_fee_amount_for_fee_receiver: 0, - }; - let position_ui_fees = PositionUiFees { - ui_fee_receiver: address_zero, ui_fee_receiver_factor: 0, ui_fee_amount: 0, + claimable_long_token_amount: *fees.funding.claimable_long_token_amount, + claimable_short_token_amount: *fees.funding.claimable_short_token_amount, + latest_funding_fee_amount_per_size: *fees.funding.latest_funding_fee_amount_per_size, + latest_long_token_claimable_funding_amount_per_size: *fees + .funding + .latest_long_token_claimable_funding_amount_per_size, + latest_short_token_claimable_funding_amount_per_size: *fees + .funding + .latest_short_token_claimable_funding_amount_per_size, }; - let price = Price { min: 0, max: 0, }; - PositionFees { - referral: position_referral_fees, - funding: position_funding_fees, - borrowing: position_borrowing_fees, - ui: position_ui_fees, - collateral_token_price: price, + let borrowing: position_pricing_utils::PositionBorrowingFees = Default::default(); + let ui: position_pricing_utils::PositionUiFees = Default::default(); + // all fees are zeroed even though funding may have been paid + // the funding fee amount value may not be accurate in the events due to this + position_pricing_utils::PositionFees { + referral, + funding, + borrowing, + ui, + collateral_token_price: *fees.collateral_token_price, position_fee_factor: 0, protocol_fee_amount: 0, position_fee_receiver_factor: 0, diff --git a/src/position/error.cairo b/src/position/error.cairo index ae307cf4..e3cd5a65 100644 --- a/src/position/error.cairo +++ b/src/position/error.cairo @@ -3,11 +3,12 @@ mod PositionError { const INVALID_POSITION_SIZE_VALUES: felt252 = 'invalid_position_size_values'; const POSITION_NOT_FOUND: felt252 = 'position_not_found'; const POSITION_INDEX_NOT_FOUND: felt252 = 'position_index_not_found'; - const CANT_BE_ZERO: felt252 = 'position account cant be 0'; - const INVALID_OUTPUT_TOKEN: felt252 = 'invalid output token'; - const MIN_POSITION_SIZE: felt252 = 'minumum position size'; - const LIQUIDATABLE_POSITION: felt252 = 'liquidatable position'; const UNEXPECTED_POSITION_STATE: felt252 = 'unexpected_position_state'; + const CANT_BE_ZERO: felt252 = 'position_account_cant_be_0'; + const INVALID_OUTPUT_TOKEN: felt252 = 'invalid_output_token'; + const MIN_POSITION_SIZE: felt252 = 'minimum_position_size'; + const LIQUIDATABLE_POSITION: felt252 = 'liquidatable_position'; + const EMPTY_HOLDING_ADDRESS: felt252 = 'empty_holding_address'; fn INVALID_DECREASE_ORDER_SIZE(size_delta_usd: u128, size_in_usd: u128) { let mut data = array!['invalid decrease order size']; @@ -26,4 +27,9 @@ mod PositionError { let data = array!['position should be liquidated']; panic(data) } + + fn INSUFFICIENT_FUNDS_TO_PAY_FOR_COSTS(remaining_cost_usd: u128, step: felt252) { + let mut data = array!['InsufficientFundsToPayForCosts', remaining_cost_usd.into(), step]; + panic(data); + } } diff --git a/src/position/position_utils.cairo b/src/position/position_utils.cairo index 5953439f..07a5d611 100644 --- a/src/position/position_utils.cairo +++ b/src/position/position_utils.cairo @@ -7,26 +7,28 @@ use starknet::{ContractAddress, contract_address_const}; use poseidon::poseidon_hash_span; // Local imports. - -use satoru::data::data_store::{IDataStoreDispatcher, IDataStoreDispatcherTrait}; +use satoru::data::{data_store::{IDataStoreDispatcher, IDataStoreDispatcherTrait}, keys}; use satoru::event::event_emitter::{IEventEmitterDispatcher, IEventEmitterDispatcherTrait}; use satoru::oracle::oracle::{IOracleDispatcher, IOracleDispatcherTrait}; use satoru::swap::swap_handler::{ISwapHandlerDispatcher, ISwapHandlerDispatcherTrait}; use satoru::market::{market::Market, market_utils::MarketPrices, market_utils}; -use satoru::data::keys; use satoru::position::{position::Position, error::PositionError}; use satoru::pricing::{ position_pricing_utils, position_pricing_utils::PositionFees, position_pricing_utils::GetPriceImpactUsdParams, position_pricing_utils::GetPositionFeesParams }; -use satoru::order::order::{Order, SecondaryOrderType}; +use satoru::order::{ + order::{Order, SecondaryOrderType}, base_order_utils::ExecuteOrderParamsContracts, + order_vault::{IOrderVaultDispatcher, IOrderVaultDispatcherTrait} +}; use satoru::mock::referral_storage::{IReferralStorageDispatcher, IReferralStorageDispatcherTrait}; -use satoru::utils::traits::ContractAddressDefault; -use satoru::order::base_order_utils::ExecuteOrderParamsContracts; use satoru::price::price::{Price, PriceTrait}; -use satoru::utils::{calc, precision, error_utils, i128::{I128Store, I128Serde, I128Div, I128Mul}}; +use satoru::utils::{ + calc, precision, i128::{I128Store, I128Serde, I128Div, I128Mul, I128Default}, + default::DefaultContractAddress, error_utils +}; use satoru::referral::referral_utils; -use satoru::order::order_vault::{IOrderVaultDispatcher, IOrderVaultDispatcherTrait}; + /// Struct used in increasePosition and decreasePosition. #[derive(Drop, Copy, starknet::Store, Serde)] struct UpdatePositionParams { @@ -78,7 +80,7 @@ struct WillPositionCollateralBeSufficientValues { } /// Struct used as decrease_position_collateral output. -#[derive(Drop, Copy, starknet::Store, Serde)] +#[derive(Drop, starknet::Store, Serde, Default, Copy)] struct DecreasePositionCollateralValuesOutput { /// The output token address. output_token: ContractAddress, @@ -91,7 +93,7 @@ struct DecreasePositionCollateralValuesOutput { } /// Struct used to contain the values in process_collateral -#[derive(Drop, Copy, starknet::Store, Serde)] +#[derive(Drop, starknet::Store, Serde, Default, Copy)] struct DecreasePositionCollateralValues { /// The order execution price. execution_price: u128, diff --git a/src/pricing/position_pricing_utils.cairo b/src/pricing/position_pricing_utils.cairo index 7636f24c..565c6bb1 100644 --- a/src/pricing/position_pricing_utils.cairo +++ b/src/pricing/position_pricing_utils.cairo @@ -23,7 +23,8 @@ use satoru::utils::{calc, precision}; use satoru::pricing::error::PricingError; use satoru::referral::referral_utils; use satoru::utils::{ - i128::{I128Store, I128Serde, I128Div, I128Mul, I128Default}, error_utils, calc::to_signed + i128::{I128Store, I128Serde, I128Div, I128Mul, I128Default}, error_utils, calc::to_signed, + default::DefaultContractAddress, }; use integer::u128_to_felt252; @@ -76,13 +77,8 @@ struct OpenInterestParams { next_short_open_interest: u128, } -impl DefaultContractAddress of Default { - fn default() -> ContractAddress { - Zeroable::zero() - } -} /// Struct to store position fees data. -#[derive(Default, Drop, Copy, starknet::Store, Serde)] +#[derive(Default, Drop, starknet::Store, Serde, Copy)] struct PositionFees { /// The referral fees. referral: PositionReferralFees, @@ -115,7 +111,7 @@ struct PositionFees { } /// Struct used to store referral parameters useful for fees computation. -#[derive(Default, Drop, Copy, starknet::Store, Serde)] +#[derive(Default, Drop, starknet::Store, Serde, Copy)] struct PositionReferralFees { /// The referral code used. referral_code: felt252, @@ -136,7 +132,7 @@ struct PositionReferralFees { } /// Struct used to store position borrowing fees. -#[derive(Default, Drop, Copy, starknet::Store, Serde)] +#[derive(Default, Drop, starknet::Store, Serde, Copy)] struct PositionBorrowingFees { /// The borrowing fees amount in USD. borrowing_fee_usd: u128, @@ -166,7 +162,7 @@ struct PositionFundingFees { } /// Struct used to store position ui fees -#[derive(Default, Drop, Copy, starknet::Store, Serde)] +#[derive(Default, Drop, starknet::Store, Serde, Copy)] struct PositionUiFees { /// The ui fee receiver address ui_fee_receiver: ContractAddress, diff --git a/src/utils/default.cairo b/src/utils/default.cairo new file mode 100644 index 00000000..25fa131d --- /dev/null +++ b/src/utils/default.cairo @@ -0,0 +1,10 @@ +// Core lib imports +use core::option::OptionTrait; +use core::traits::TryInto; +use starknet::ContractAddress; + +impl DefaultContractAddress of Default { + fn default() -> ContractAddress { + 0.try_into().unwrap() + } +} diff --git a/tests/data/test_position.cairo b/tests/data/test_position.cairo index a78346fa..24a1bab5 100644 --- a/tests/data/test_position.cairo +++ b/tests/data/test_position.cairo @@ -63,7 +63,7 @@ fn given_normal_conditions_when_set_position_new_and_override_then_works() { #[test] -#[should_panic(expected: ('position account cant be 0',))] +#[should_panic(expected: ('position_account_cant_be_0',))] fn given_position_account_0_when_set_position_then_fails() { // Setup let (caller_address, role_store, data_store) = setup(); diff --git a/tests/event/test_market_events_emitted.cairo b/tests/event/test_market_events_emitted.cairo index 44e1d77e..9f2484a8 100644 --- a/tests/event/test_market_events_emitted.cairo +++ b/tests/event/test_market_events_emitted.cairo @@ -145,7 +145,7 @@ fn given_normal_conditions_when_emit_position_impact_pool_amount_updated_then_wo // Create dummy data. let market = contract_address_const::<'market'>(); - let delta: u128 = 1; + let delta: i128 = 1; let next_value: u128 = 2; // Create the expected data. diff --git a/tests/market/test_market_utils.cairo b/tests/market/test_market_utils.cairo index 19487beb..56dcfe45 100644 --- a/tests/market/test_market_utils.cairo +++ b/tests/market/test_market_utils.cairo @@ -409,19 +409,22 @@ fn given_normal_conditions_when_increment_claimable_collateral_amount_then_works // Fill required data store keys. data_store.set_u128(keys::claimable_collateral_time_divisor(), 1); - // Actual test case. - market_utils::increment_claimable_collateral_amount( - data_store, chain, event_emitter, market_address, token, account, delta - ); + // Actual test case. + // TODO uncomment below when we can use get_block_timestamp() with foundry + // market_utils::increment_claimable_collateral_amount( + // data_store, event_emitter, market_address, token, account, delta + // ); // Perform assertions. // The value of the claimable collateral amount for the account should now be 50. // Read the value from the data store using the hardcoded key and assert it. - assert(data_store.get_u128(claimable_collatoral_amount_for_account_key) == 50, 'wrong value'); + // TODO uncomment below when we can use get_block_timestamp() with foundry + //assert(data_store.get_u128(claimable_collatoral_amount_for_account_key) == 50, 'wrong value'); // The value of the claimable collateral amount for the market should now be 50. // Read the value from the data store using the hardcoded key and assert it. - assert(data_store.get_u128(claimable_collateral_amount_key) == 50, 'wrong value'); + // TODO uncomment below when we can use get_block_timestamp() with foundry + //assert(data_store.get_u128(claimable_collateral_amount_key) == 50, 'wrong value'); // ********************************************************************************************* // * TEARDOWN * diff --git a/tests/position/test_decrease_position_collateral_utils.cairo b/tests/position/test_decrease_position_collateral_utils.cairo new file mode 100644 index 00000000..2eb02cf2 --- /dev/null +++ b/tests/position/test_decrease_position_collateral_utils.cairo @@ -0,0 +1,240 @@ +// Core lib imports. +use array::ArrayTrait; +use core::traits::{Into, TryInto}; +use snforge_std::{declare, ContractClassTrait, start_prank}; +use starknet::{ContractAddress, contract_address_const}; + +// Local imports. +use satoru::data::{data_store::{IDataStoreDispatcher, IDataStoreDispatcherTrait}, keys}; +use satoru::event::event_emitter::IEventEmitterDispatcher; +use satoru::market::{market::Market, market_utils::MarketPrices}; +use satoru::mock::referral_storage::IReferralStorageDispatcher; +use satoru::oracle::oracle::IOracleDispatcher; +use satoru::order::{ + order::{DecreasePositionSwapType, Order, OrderType, SecondaryOrderType}, + base_order_utils::{ExecuteOrderParams, ExecuteOrderParamsContracts}, + order_vault::IOrderVaultDispatcher +}; +use satoru::position::{ + position_utils::{UpdatePositionParams, DecreasePositionCache, DecreasePositionCollateralValues}, + position::Position, decrease_position_collateral_utils +}; +use satoru::price::price::Price; +use satoru::swap::swap_handler::ISwapHandlerDispatcher; +use satoru::tests_lib::{setup, teardown, setup_event_emitter}; +use satoru::utils::span32::{Span32, Array32Trait}; + +/// Utility function to deploy a `SwapHandler` contract and return its dispatcher. +fn deploy_swap_handler_address(role_store_address: ContractAddress) -> ContractAddress { + let contract = declare('SwapHandler'); + let constructor_calldata = array![role_store_address.into()]; + contract.deploy(@constructor_calldata).unwrap() +} + +fn deploy_token() -> ContractAddress { + let contract = declare('ERC20'); + let constructor_calldata = array!['Test', 'TST', 1000000, 0, 0x101]; + contract.deploy(@constructor_calldata).unwrap() +} + +/// Utility function to deploy a `ReferralStorage` contract and return its dispatcher. +fn deploy_referral_storage(event_emitter_address: ContractAddress) -> ContractAddress { + let contract = declare('ReferralStorage'); + let constructor_calldata = array![event_emitter_address.into()]; + contract.deploy(@constructor_calldata).unwrap() +} + +#[test] +fn given_good_params_when_process_collateral_then_succeed() { + // + // Setup + // + let (caller_address, role_store, data_store) = setup(); + let (event_emitter_address, event_emitter) = setup_event_emitter(); + let long_token_address = deploy_token(); + + // setting open_interest to 10_000 to allow decreasing position. + data_store + .set_u128( + keys::open_interest_key( + contract_address_const::<'market_token'>(), long_token_address, true + ), + 10_000 + ); + let swap_handler_address = deploy_swap_handler_address(role_store.contract_address); + let swap_handler = ISwapHandlerDispatcher { contract_address: swap_handler_address }; + let referral_storage_address = deploy_referral_storage(event_emitter_address); + + let params = create_new_update_position_params( + DecreasePositionSwapType::SwapCollateralTokenToPnlToken, + swap_handler, + data_store.contract_address, + event_emitter_address, + referral_storage_address, + long_token_address, + ); + + let values = create_new_decrease_position_cache(long_token_address); + + // + // Execution + // + let result = decrease_position_collateral_utils::process_collateral( + event_emitter, params, values + ); + + // Checks + let open_interest = data_store + .get_u128( + keys::open_interest_key( + contract_address_const::<'market_token'>(), long_token_address, true + ), + ); +} + +#[test] +fn given_good_params_get_execution_price_then_succeed() { + // + // Setup + // + let (caller_address, role_store, data_store) = setup(); + let (event_emitter_address, event_emitter) = setup_event_emitter(); + let long_token_address = deploy_token(); + + // setting open_interest to 10_000 to allow decreasing position. + data_store + .set_u128( + keys::open_interest_key( + contract_address_const::<'market_token'>(), long_token_address, true + ), + 10_000 + ); + let swap_handler_address = deploy_swap_handler_address(role_store.contract_address); + let swap_handler = ISwapHandlerDispatcher { contract_address: swap_handler_address }; + let referral_storage_address = deploy_referral_storage(event_emitter_address); + + let params = create_new_update_position_params( + DecreasePositionSwapType::SwapCollateralTokenToPnlToken, + swap_handler, + data_store.contract_address, + event_emitter_address, + referral_storage_address, + long_token_address + ); + + // + // Execution + // + let (_, _, execution_price) = decrease_position_collateral_utils::get_execution_price( + params, Price { min: 10, max: 10 } + ); + // + // Checks + // + assert(execution_price > 0, 'no execution price'); + teardown(data_store.contract_address); +} + +/// Utility function to create new UpdatePositionParams struct +fn create_new_update_position_params( + decrease_position_swap_type: DecreasePositionSwapType, + swap_handler: ISwapHandlerDispatcher, + data_store_address: ContractAddress, + event_emitter_address: ContractAddress, + referral_storage_address: ContractAddress, + long_token_address: ContractAddress +) -> UpdatePositionParams { + let order_vault = contract_address_const::<'order_vault'>(); + let oracle = contract_address_const::<'oracle'>(); + let contracts = ExecuteOrderParamsContracts { + data_store: IDataStoreDispatcher { contract_address: data_store_address }, + event_emitter: IEventEmitterDispatcher { contract_address: event_emitter_address }, + order_vault: IOrderVaultDispatcher { contract_address: order_vault }, + oracle: IOracleDispatcher { contract_address: oracle }, + swap_handler, + referral_storage: IReferralStorageDispatcher { contract_address: referral_storage_address } + }; + + let market = Market { + market_token: contract_address_const::<'market_token'>(), + index_token: long_token_address, + long_token: long_token_address, + short_token: contract_address_const::<'short_token'>() + }; + + let order = Order { + key: 123456789, + order_type: OrderType::StopLossDecrease, + decrease_position_swap_type, + account: contract_address_const::<'account'>(), + receiver: contract_address_const::<'receiver'>(), + callback_contract: contract_address_const::<'callback_contract'>(), + ui_fee_receiver: contract_address_const::<'ui_fee_receiver'>(), + market: contract_address_const::<'market'>(), + initial_collateral_token: contract_address_const::<'token1'>(), + swap_path: array![ + contract_address_const::<'swap_path_0'>(), contract_address_const::<'swap_path_1'>() + ] + .span32(), + size_delta_usd: 1000, + initial_collateral_delta_amount: 1000, + trigger_price: 11111, + acceptable_price: 11111, + execution_fee: 10, + callback_gas_limit: 300000, + min_output_amount: 10, + updated_at_block: 1, + is_long: true, + is_frozen: false + }; + + let position = Position { + key: 123456789, + account: contract_address_const::<'account'>(), + market: contract_address_const::<'market'>(), + collateral_token: contract_address_const::<'collateral_token'>(), + size_in_usd: 1000, + size_in_tokens: 1000, + collateral_amount: 1000, + borrowing_factor: 1, + funding_fee_amount_per_size: 1, + long_token_claimable_funding_amount_per_size: 10, + short_token_claimable_funding_amount_per_size: 10, + increased_at_block: 1, + decreased_at_block: 3, + is_long: false, + }; + + let params = UpdatePositionParams { + contracts, + market, + order, + order_key: 123456789, + position, + position_key: 123456789, + secondary_order_type: SecondaryOrderType::None + }; + + params +} + +/// Utility function to create new DecreasePositionCache struct +fn create_new_decrease_position_cache( + long_token_address: ContractAddress +) -> DecreasePositionCache { + let price = Price { min: 1, max: 1 }; + DecreasePositionCache { + prices: MarketPrices { + index_token_price: price, long_token_price: price, short_token_price: price, + }, + estimated_position_pnl_usd: 100, + estimated_realized_pnl_usd: 0, + estimated_remaining_pnl_usd: 100, + pnl_token: long_token_address, + pnl_token_price: price, + collateral_token_price: price, + initial_collateral_amount: 100, + next_position_size_in_usd: 500, + next_position_borrowing_factor: 100000, + } +} diff --git a/tests/position/test_position_utils.cairo b/tests/position/test_position_utils.cairo index b17a1a8b..c259620f 100644 --- a/tests/position/test_position_utils.cairo +++ b/tests/position/test_position_utils.cairo @@ -148,8 +148,8 @@ fn given_empty_market_when_validate_position_then_fails() { #[test] -#[should_panic(expected: ('minumum position size',))] -fn given_minumum_position_size_when_validate_position_then_fails() { +#[should_panic(expected: ('minimum_position_size',))] +fn given_minimum_position_size_when_validate_position_then_fails() { // // Setup //