diff --git a/libraries/chain/asset_evaluator.cpp b/libraries/chain/asset_evaluator.cpp index 0781321eac..4e95b1adfa 100644 --- a/libraries/chain/asset_evaluator.cpp +++ b/libraries/chain/asset_evaluator.cpp @@ -46,6 +46,12 @@ namespace detail { } } + void check_bitasset_options_bsip74( const fc::time_point_sec& block_time, const bitasset_options& options) + { + FC_ASSERT( block_time >= HARDFORK_CORE_BSIP74_TIME + || !options.extensions.value.margin_call_fee_ratio.valid(), + "A BitAsset's MCFR cannot be set before Hardfork BSIP74" ); + } // TODO review and remove code below and links to it after HARDFORK_BSIP_81_TIME void check_asset_options_hf_bsip81(const fc::time_point_sec& block_time, const asset_options& options) { @@ -131,7 +137,10 @@ void_result asset_create_evaluator::do_evaluate( const asset_create_operation& o if( op.bitasset_opts ) { + detail::check_bitasset_options_bsip74(d.head_block_time(), *op.bitasset_opts); + const asset_object& backing = op.bitasset_opts->short_backing_asset(d); + if( backing.is_market_issued() ) { const asset_bitasset_data_object& backing_bitasset_data = backing.bitasset_data(d); @@ -484,6 +493,8 @@ void_result asset_update_bitasset_evaluator::do_evaluate(const asset_update_bita // hf 922_931 is a consensus/logic change. This hf cannot be removed. bool after_hf_core_922_931 = ( d.get_dynamic_global_properties().next_maintenance_time > HARDFORK_CORE_922_931_TIME ); + detail::check_bitasset_options_bsip74(d.head_block_time(), op.new_options); + // Are we changing the backing asset? if( op.new_options.short_backing_asset != current_bitasset_data.options.short_backing_asset ) { @@ -829,8 +840,8 @@ operation_result asset_settle_evaluator::do_apply(const asset_settle_evaluator:: // performance loss. Needs testing. if( d.head_block_time() >= HARDFORK_CORE_1780_TIME ) { - const bool is_maker = false; // Settlement orders are takers - auto issuer_fees = d.pay_market_fees( fee_paying_account, settled_amount.asset_id(d), settled_amount , is_maker ); + auto issuer_fees = d.pay_market_fees( fee_paying_account, settled_amount.asset_id(d), + settled_amount, false ); settled_amount -= issuer_fees; } diff --git a/libraries/chain/asset_object.cpp b/libraries/chain/asset_object.cpp index e442465d36..b47c21cad2 100644 --- a/libraries/chain/asset_object.cpp +++ b/libraries/chain/asset_object.cpp @@ -75,6 +75,7 @@ void graphene::chain::asset_bitasset_data_object::update_median_feeds( time_poin // update data derived from ICR current_initial_collateralization = price(); } + adjust_mcfr(); return; } if( current_feeds.size() == 1 ) @@ -90,6 +91,7 @@ void graphene::chain::asset_bitasset_data_object::update_median_feeds( time_poin // update data derived from ICR refresh_current_initial_collateralization(); } + adjust_mcfr(); return; } @@ -118,6 +120,7 @@ void graphene::chain::asset_bitasset_data_object::update_median_feeds( time_poin // update data derived from ICR refresh_current_initial_collateralization(); } + adjust_mcfr(); } void asset_bitasset_data_object::refresh_current_initial_collateralization() @@ -204,6 +207,28 @@ string asset_object::amount_to_string(share_type amount) const return result; } +uint16_t asset_bitasset_data_object::adjust_mcfr() +{ + if (options.extensions.value.margin_call_fee_ratio.valid()) + { + auto mcfr = *options.extensions.value.margin_call_fee_ratio; + // Reduce mcfr if it causes parameters to go out of range + if ( ( 1 > current_feed.maximum_short_squeeze_ratio + || current_feed.maximum_short_squeeze_ratio >= current_feed.maintenance_collateral_ratio) + || (1 > current_feed.maximum_short_squeeze_ratio - mcfr + || current_feed.maximum_short_squeeze_ratio - mcfr >= current_feed.maintenance_collateral_ratio)) + mcfr = current_feed.maximum_short_squeeze_ratio - 1; + options.extensions.value.current_margin_call_fee_ratio = mcfr; + return mcfr; + } + else + { + options.extensions.value.current_margin_call_fee_ratio = fc::optional(); + } + + return 0; +} + FC_REFLECT_DERIVED_NO_TYPENAME( graphene::chain::asset_dynamic_data_object, (graphene::db::object), (current_supply)(confidential_supply)(accumulated_fees)(accumulated_collateral_fees)(fee_pool) ) diff --git a/libraries/chain/db_market.cpp b/libraries/chain/db_market.cpp index c3a03fa9f3..085da5edb2 100644 --- a/libraries/chain/db_market.cpp +++ b/libraries/chain/db_market.cpp @@ -45,6 +45,14 @@ namespace detail { return static_cast(a); } + share_type calculate_ratio( const share_type& value, uint16_t ratio) + { + fc::uint128_t a(value.value); + a *= (ratio-GRAPHENE_COLLATERAL_RATIO_DENOM); + a /= GRAPHENE_COLLATERAL_RATIO_DENOM; + return static_cast(a); + } + } //detail /** @@ -474,10 +482,7 @@ bool database::apply_order(const limit_order_object& new_order_object, bool allo && !sell_abd->has_settlement() && !sell_abd->current_feed.settlement_price.is_null() ) { - if( before_core_hardfork_1270 ) - call_match_price = ~sell_abd->current_feed.max_short_squeeze_price_before_hf_1270(); - else - call_match_price = ~sell_abd->current_feed.max_short_squeeze_price(); + call_match_price = ~get_max_short_squeeze_price( maint_time, sell_abd->current_feed ); if( ~new_order_object.sell_price <= call_match_price ) // new limit order price is good enough to match a call to_check_call_orders = true; } @@ -685,8 +690,9 @@ int database::match( const limit_order_object& bid, const call_order_object& ask order_pays = call_receives; int result = 0; - result |= fill_limit_order( bid, order_pays, order_receives, cull_taker, match_price, false ); // the limit order is taker - result |= fill_call_order( ask, call_pays, call_receives, match_price, true ) << 1; // the call order is maker + result |= fill_limit_order( bid, order_pays, order_receives, cull_taker, match_price, + false ); // limit order is the taker + result |= fill_call_order( ask, call_pays, call_receives, match_price, true ) << 1; // the call order is maker // result can be 0 when call order has target_collateral_ratio option set. return result; @@ -803,9 +809,8 @@ bool database::fill_limit_order( const limit_order_object& order, const asset& p FC_ASSERT( pays.asset_id != receives.asset_id ); const account_object& seller = order.seller(*this); - const asset_object& recv_asset = receives.asset_id(*this); - auto issuer_fees = pay_market_fees(&seller, recv_asset, receives, is_maker); + const auto issuer_fees = pay_market_fees(&seller, receives.asset_id(*this), receives, is_maker); pay_order( seller, receives - issuer_fees, pays ); @@ -897,11 +902,19 @@ bool database::fill_limit_order( const limit_order_object& order, const asset& p return maybe_cull_small_order( *this, order ); return false; } -} FC_CAPTURE_AND_RETHROW( (order)(pays)(receives) ) } - +} FC_CAPTURE_AND_RETHROW( (order)(pays)(receives) ) } +/*** + * @brief fill a call order in the specified amounts + * @param order the call order + * @param pays What the call order will give to the other party (collateral) + * @param receives what the call order will receive from the other party (debt) + * @param fill_price the price at which the call order will execute + * @param is_maker TRUE if the call order is the maker, FALSE if it is the taker + * @returns TRUE if the call order was completely filled + */ bool database::fill_call_order( const call_order_object& order, const asset& pays, const asset& receives, - const price& fill_price, const bool is_maker ) + const price& fill_price, const bool is_maker ) { try { FC_ASSERT( order.debt_type() == receives.asset_id ); FC_ASSERT( order.collateral_type() == pays.asset_id ); @@ -910,53 +923,60 @@ bool database::fill_call_order( const call_order_object& order, const asset& pay // TODO pass in mia and bitasset_data for better performance const asset_object& mia = receives.asset_id(*this); FC_ASSERT( mia.is_market_issued() ); + const asset_bitasset_data_object& bitasset = mia.bitasset_data(*this); + + // calculate any margin call fees NOTE: Paid in collateral asset + const auto margin_fee = pay_margin_fees(mia, pays); optional collateral_freed; - modify( order, [&]( call_order_object& o ){ - o.debt -= receives.amount; - o.collateral -= pays.amount; - if( o.debt == 0 ) + // adjust the order + modify( order, [&]( call_order_object& o ) { + o.debt -= receives.amount; + o.collateral -= pays.amount + margin_fee.amount; + if( o.debt == 0 ) // is the whole debt paid? + { + collateral_freed = o.get_collateral(); + o.collateral = 0; + } + else // the debt was not completely paid + { + auto maint_time = get_dynamic_global_properties().next_maintenance_time; + // update call_price after core-343 hard fork, + // but don't update call_price after core-1270 hard fork + if( maint_time <= HARDFORK_CORE_1270_TIME && maint_time > HARDFORK_CORE_343_TIME ) { - collateral_freed = o.get_collateral(); - o.collateral = 0; - } - else - { - auto maint_time = get_dynamic_global_properties().next_maintenance_time; - // update call_price after core-343 hard fork, - // but don't update call_price after core-1270 hard fork - if( maint_time <= HARDFORK_CORE_1270_TIME && maint_time > HARDFORK_CORE_343_TIME ) - { - o.call_price = price::call_price( o.get_debt(), o.get_collateral(), - mia.bitasset_data(*this).current_feed.maintenance_collateral_ratio ); - } + o.call_price = price::call_price( o.get_debt(), o.get_collateral(), + bitasset.current_feed.maintenance_collateral_ratio ); } + } }); // update current supply const asset_dynamic_data_object& mia_ddo = mia.dynamic_asset_data_id(*this); - modify( mia_ddo, [&receives]( asset_dynamic_data_object& ao ){ ao.current_supply -= receives.amount; }); - // Adjust balance + // If the whole debt is paid, adjust borrower's collateral balance if( collateral_freed.valid() ) adjust_balance( order.borrower, *collateral_freed ); // Update account statistics. We know that order.collateral_type() == pays.asset_id if( pays.asset_id == asset_id_type() ) { - modify( get_account_stats_by_owner(order.borrower), [&collateral_freed,&pays]( account_statistics_object& b ){ - b.total_core_in_orders -= pays.amount; + modify( get_account_stats_by_owner(order.borrower), + [&collateral_freed,&pays,&margin_fee]( account_statistics_object& b ){ + b.total_core_in_orders -= pays.amount + margin_fee.amount; if( collateral_freed.valid() ) b.total_core_in_orders -= collateral_freed->amount; }); } + // virtual operation for account history push_applied_operation( fill_order_operation( order.id, order.borrower, pays, receives, - asset(0, pays.asset_id), fill_price, is_maker ) ); + margin_fee, fill_price, is_maker ) ); + // Call order completely filled, remove it if( collateral_freed.valid() ) remove( order ); @@ -1034,6 +1054,20 @@ bool database::fill_settle_order( const force_settlement_object& settle, const a } FC_CAPTURE_AND_RETHROW( (settle)(pays)(receives) ) } +/*** + * Get the correct max_short_squeeze_price from the price_feed based on chain time + * (due to hardfork changes in the calculation) + * @param block_time the chain's current block time + * @param feed the debt asset's price feed + * @returns the max short squeeze price + */ +price database::get_max_short_squeeze_price( const fc::time_point_sec& block_time, const price_feed& feed)const +{ + if ( block_time <= HARDFORK_CORE_1270_TIME ) + return feed.max_short_squeeze_price_before_hf_1270(); + return feed.max_short_squeeze_price(); +} + /** * Starting with the least collateralized orders, fill them if their * call price is above the max(lowest bid,call_limit). @@ -1082,8 +1116,7 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa // looking for limit orders selling the most USD for the least CORE auto max_price = price::max( mia.id, bitasset.options.short_backing_asset ); // stop when limit orders are selling too little USD for too much CORE - auto min_price = ( before_core_hardfork_1270 ? bitasset.current_feed.max_short_squeeze_price_before_hf_1270() - : bitasset.current_feed.max_short_squeeze_price() ); + auto min_price = get_max_short_squeeze_price( maint_time, bitasset.current_feed); // NOTE limit_price_index is sorted from greatest to least auto limit_itr = limit_price_index.lower_bound( max_price ); @@ -1181,10 +1214,10 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa } asset usd_for_sale = limit_order.amount_for_sale(); - asset call_pays, call_receives, order_pays, order_receives; + asset call_pays, call_receives, limit_pays, limit_receives; if( usd_to_buy > usd_for_sale ) { // fill order - order_receives = usd_for_sale * match_price; // round down, in favor of call order + limit_receives = usd_for_sale * match_price; // round down, in favor of call order // Be here, the limit order won't be paying something for nothing, since if it would, it would have // been cancelled elsewhere already (a maker limit order won't be paying something for nothing): @@ -1201,7 +1234,7 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa // so we should cull the order in fill_limit_order() below. // The order would receive 0 even at `match_price`, so it would receive 0 at its own price, // so calling maybe_cull_small() will always cull it. - call_receives = order_receives.multiply_and_round_up( match_price ); + call_receives = limit_receives.multiply_and_round_up( match_price ); filled_limit = true; @@ -1210,10 +1243,10 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa if( before_core_hardfork_342 ) { - order_receives = usd_to_buy * match_price; // round down, in favor of call order + limit_receives = usd_to_buy * match_price; // round down, in favor of call order } else - order_receives = usd_to_buy.multiply_and_round_up( match_price ); // round up, in favor of limit order + limit_receives = usd_to_buy.multiply_and_round_up( match_price ); // round up, in favor of limit order filled_call = true; // this is safe, since BSIP38 (hard fork core-834) depends on BSIP31 (hard fork core-343) @@ -1227,8 +1260,8 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa } } - call_pays = order_receives; - order_pays = call_receives; + call_pays = limit_receives; + limit_pays = call_receives; if( filled_call && before_core_hardfork_343 ) ++call_price_itr; @@ -1241,7 +1274,7 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa auto next_limit_itr = std::next( limit_itr ); // when for_new_limit_order is true, the limit order is taker, otherwise the limit order is maker - bool really_filled = fill_limit_order( limit_order, order_pays, order_receives, true, match_price, !for_new_limit_order ); + bool really_filled = fill_limit_order( limit_order, limit_pays, limit_receives, true, match_price, !for_new_limit_order ); if( really_filled || ( filled_limit && before_core_hardfork_453 ) ) limit_itr = next_limit_itr; @@ -1295,6 +1328,39 @@ asset database::calculate_market_fee( const asset_object& trade_asset, const ass return percent_fee; } +/** + * @brief Calculate the margin fee that is to be taken + * @param debt the indebted asset + * @param collateral the amount of collateral received (before fees) + * @returns the amount of fee that should be collected + */ +asset database::calculate_margin_fee(const asset_object& debt, const asset& collateral)const +{ + auto ba = debt.bitasset_data(*this); + auto price_feed = ba.current_feed; + if ( price_feed.settlement_price.is_null() + || price_feed.settlement_price.base.amount == 0 + || !ba.options.extensions.value.current_margin_call_fee_ratio.valid()) + return asset(0); + auto amount = detail::calculate_ratio( collateral.amount, + *ba.options.extensions.value.current_margin_call_fee_ratio ); + return asset(amount, collateral.asset_id) ; +} + +/**** + * @brief calculate the margin fee and distribute it + * @param debt_asset the indebted asset + * @param collarteral the amount of the collateral + * @returns the amount of the fee that was collected + */ +asset database::pay_margin_fees(const asset_object& debt_asset, const asset& collateral) +{ + const auto margin_fees = calculate_margin_fee( debt_asset, collateral ); + if (margin_fees.amount.value != 0) + debt_asset.accumulate_fee(*this, margin_fees); + return margin_fees; +} + asset database::pay_market_fees(const account_object* seller, const asset_object& recv_asset, const asset& receives, const bool& is_maker) { diff --git a/libraries/chain/db_update.cpp b/libraries/chain/db_update.cpp index f98f09de6b..3ff314966d 100644 --- a/libraries/chain/db_update.cpp +++ b/libraries/chain/db_update.cpp @@ -219,12 +219,8 @@ bool database::check_for_blackswan( const asset_object& mia, bool enable_black_s return false; price highest = settle_price; - if( maint_time > HARDFORK_CORE_1270_TIME ) - // due to #338, we won't check for black swan on incoming limit order, so need to check with MSSP here - highest = bitasset.current_feed.max_short_squeeze_price(); - else if( maint_time > HARDFORK_CORE_338_TIME ) - // due to #338, we won't check for black swan on incoming limit order, so need to check with MSSP here - highest = bitasset.current_feed.max_short_squeeze_price_before_hf_1270(); + if (maint_time > HARDFORK_CORE_338_TIME) + highest = get_max_short_squeeze_price( maint_time, bitasset.current_feed); const limit_order_index& limit_index = get_index_type(); const auto& limit_price_index = limit_index.indices().get(); diff --git a/libraries/chain/hardfork.d/CORE_BSIP74.hf b/libraries/chain/hardfork.d/CORE_BSIP74.hf new file mode 100644 index 0000000000..4825cc86b1 --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_BSIP74.hf @@ -0,0 +1,4 @@ +// bitshares-core BSIP 74 add margin call fee +#ifndef HARDFORK_CORE_BSIP74_TIME +#define HARDFORK_CORE_BSIP74_TIME (fc::time_point_sec( 1679955066 ) ) // Temporary date until actual hardfork date is set +#endif diff --git a/libraries/chain/include/graphene/chain/asset_object.hpp b/libraries/chain/include/graphene/chain/asset_object.hpp index 97255a9610..30f7d0bd8e 100644 --- a/libraries/chain/include/graphene/chain/asset_object.hpp +++ b/libraries/chain/include/graphene/chain/asset_object.hpp @@ -302,13 +302,22 @@ namespace graphene { namespace chain { * * This calculates the median feed from @ref feeds, feed_lifetime_sec * in @ref options, and the given parameters. - * It may update the current_feed_publication_time, current_feed and - * current_maintenance_collateralization member variables. + * It may update the current_feed_publication_time, current_feed, + * current_maintenance_collateralization, and options.extensions.current_margin_call_fee_ratio + * member variables. * * @param current_time the current time to use in the calculations * @param next_maintenance_time the next chain maintenance time */ void update_median_feeds(time_point_sec current_time, time_point_sec next_maintenance_time); + + /*** + * @brief Examines the current state of the asset and feed, and adjusts current_mcfr (margin call fee ratio) + * if it is out of bounds. This should be called whenever + * bitasset_options.extensions.value.margin_call_fee_ratio or the feed_price changes. + * @returns the adjusted mcfr + */ + uint16_t adjust_mcfr(); }; // key extractor for short backing asset diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index b11cb2e983..e61de62530 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -410,6 +410,16 @@ namespace graphene { namespace chain { */ ///@{ int match( const limit_order_object& taker, const limit_order_object& maker, const price& trade_price ); + /*** + * @brief Match the two orders + * @param taker the account that is removing liquidity from the book + * @param maker the account that put liquidity on the book + * @param trade_price the price the trade should execute at + * @param feed_price the price of the current feed + * @param maintenance_collateral_ratio the maintenance collateral ratio + * @param maintenance_collateralization the maintenance collateralization + * @returns 0 if no orders were matched, 1 if taker was filled, 2 if maker was filled, 3 if both were filled + */ int match( const limit_order_object& taker, const call_order_object& maker, const price& trade_price, const price& feed_price, const uint16_t maintenance_collateral_ratio, const optional& maintenance_collateralization ); @@ -424,10 +434,26 @@ namespace graphene { namespace chain { const price& fill_price); /** + * @brief fills limit order + * @param order the order + * @param pays what the account is paying + * @param receives what the account is receiving + * @param cull_if_small take care of dust + * @param fill_price the transaction price + * @param is_maker TRUE if this order is maker, FALSE if taker * @return true if the order was completely filled and thus freed. */ - bool fill_limit_order( const limit_order_object& order, const asset& pays, const asset& receives, bool cull_if_small, - const price& fill_price, const bool is_maker ); + bool fill_limit_order( const limit_order_object& order, const asset& pays, const asset& receives, + bool cull_if_small, const price& fill_price, const bool is_maker ); + /*** + * @brief attempt to fill a call order + * @param order the order + * @param pays what the buyer pays for the collateral + * @param receives the collateral received by the buyer + * @param fill_price the price the transaction executed at + * @param is_maker TRUE if the buyer is the maker, FALSE if the buyer is the taker + * @returns TRUE if the order was completely filled + */ bool fill_call_order( const call_order_object& order, const asset& pays, const asset& receives, const price& fill_price, const bool is_maker ); bool fill_settle_order( const force_settlement_object& settle, const asset& pays, const asset& receives, @@ -439,7 +465,27 @@ namespace graphene { namespace chain { // helpers to fill_order void pay_order( const account_object& receiver, const asset& receives, const asset& pays ); - asset calculate_market_fee(const asset_object& recv_asset, const asset& trade_amount, const bool& is_maker); + /** + * @brief Calculate the market fee that is to be taken + * @param trade_asset the asset (passed in to avoid a lookup) + * @param trade_amount the quantity that the fee calculation is based upon + * @param is_maker TRUE if this is the fee for a maker, FALSE if taker + */ + asset calculate_market_fee( const asset_object& trade_asset, const asset& trade_amount, const bool& is_maker); + /** + * @brief Calculate the market fee that is to be taken + * @param debt the indebted asset + * @param receives the amount of collateral received (before fees) + * @returns the amount of fee that should be collected + */ + asset calculate_margin_fee( const asset_object& debt, const asset& receives)const; + /**** + * @brief distribute the margin fee + * @param debt_asset the indebted asset + * @param receives the collateral received (before fees) + * @returns the amount of the fee that was collected + */ + asset pay_margin_fees(const asset_object& debt, const asset& receives ); asset pay_market_fees(const account_object* seller, const asset_object& recv_asset, const asset& receives, const bool& is_maker); asset pay_force_settle_fees(const asset_object& collecting_asset, const asset& collat_receives); @@ -627,6 +673,15 @@ namespace graphene { namespace chain { const chain_property_object* _p_chain_property_obj = nullptr; const witness_schedule_object* _p_witness_schedule_obj = nullptr; ///@} + protected: + /*** + * Get the correct max_short_squeeze_price from the price_feed based on chain time + * (due to hardfork changes in the calculation) + * @param block_time the chain's current block time + * @param feed the debt asset's price feed + * @returns the max short squeeze price + */ + price get_max_short_squeeze_price( const fc::time_point_sec& block_time, const price_feed& feed)const; }; namespace detail diff --git a/libraries/chain/proposal_evaluator.cpp b/libraries/chain/proposal_evaluator.cpp index d73e343759..49df7551a0 100644 --- a/libraries/chain/proposal_evaluator.cpp +++ b/libraries/chain/proposal_evaluator.cpp @@ -30,6 +30,7 @@ namespace graphene { namespace chain { namespace detail { void check_asset_options_hf_1774(const fc::time_point_sec& block_time, const asset_options& options); + void check_bitasset_options_bsip74( const fc::time_point_sec& block_time, const bitasset_options& options); void check_bitasset_options_hf_bsip77(const fc::time_point_sec& block_time, const bitasset_options& options); void check_asset_options_hf_bsip81(const fc::time_point_sec& block_time, const asset_options& options); void check_bitasset_options_hf_bsip87(const fc::time_point_sec& block_time, @@ -52,34 +53,28 @@ struct proposal_operation_hardfork_visitor void operator()(const T &v) const {} void operator()(const graphene::chain::asset_create_operation &v) const { - detail::check_asset_options_hf_1774(block_time, v.common_options); detail::check_asset_options_hf_bsip81(block_time, v.common_options); if( v.bitasset_opts.valid() ) { detail::check_bitasset_options_hf_bsip77( block_time, *v.bitasset_opts ); detail::check_bitasset_options_hf_bsip87( block_time, *v.bitasset_opts ); // HF_REMOVABLE + detail::check_bitasset_options_bsip74( block_time, *v.bitasset_opts ); } - } void operator()(const graphene::chain::asset_update_operation &v) const { - detail::check_asset_options_hf_1774(block_time, v.new_options); detail::check_asset_options_hf_bsip81(block_time, v.new_options); - } void operator()(const graphene::chain::asset_update_bitasset_operation &v) const { - detail::check_bitasset_options_hf_bsip77( block_time, v.new_options ); detail::check_bitasset_options_hf_bsip87( block_time, v.new_options ); // HF_REMOVABLE - + detail::check_bitasset_options_bsip74( block_time, v.new_options ); } void operator()(const graphene::chain::asset_claim_fees_operation &v) const { - detail::check_asset_claim_fees_hardfork_87_74_collatfee(block_time, v); // HF_REMOVABLE - } void operator()(const graphene::chain::committee_member_update_global_parameters_operation &op) const { diff --git a/libraries/protocol/asset.cpp b/libraries/protocol/asset.cpp index 8588d91c3a..6ef009b766 100644 --- a/libraries/protocol/asset.cpp +++ b/libraries/protocol/asset.cpp @@ -283,7 +283,7 @@ namespace graphene { namespace protocol { price price_feed::max_short_squeeze_price()const { - // settlement price is in debt/collateral + // note that settlement price is in the format debt/collateral return settlement_price * ratio_type( GRAPHENE_COLLATERAL_RATIO_DENOM, maximum_short_squeeze_ratio ); } diff --git a/libraries/protocol/include/graphene/protocol/asset.hpp b/libraries/protocol/include/graphene/protocol/asset.hpp index 674b130ec1..8ef6c35eeb 100644 --- a/libraries/protocol/include/graphene/protocol/asset.hpp +++ b/libraries/protocol/include/graphene/protocol/asset.hpp @@ -192,14 +192,16 @@ namespace graphene { namespace protocol { /** Fixed point between 1.000 and 10.000, implied fixed point denominator is GRAPHENE_COLLATERAL_RATIO_DENOM */ uint16_t maximum_short_squeeze_ratio = GRAPHENE_DEFAULT_MAX_SHORT_SQUEEZE_RATIO; - /** When selling collateral to pay off debt, the least amount of debt to receive should be + /** + * When selling collateral to pay off debt, the least amount of debt to receive should be * min_usd = max_short_squeeze_price() * collateral * * This is provided to ensure that a black swan cannot be trigged due to poor liquidity alone, it * must be confirmed by having the max_short_squeeze_price() move below the black swan price. + * @returns the Maximum Short Squeeze price for this asset */ price max_short_squeeze_price()const; - /// Another implementation of max_short_squeeze_price() before the core-1270 hard fork + /// Other implementation of max_short_squeeze_price() due to hardfork changes price max_short_squeeze_price_before_hf_1270()const; /// Call orders with collateralization (aka collateral/debt) not greater than this value are in margin call territory. diff --git a/libraries/protocol/include/graphene/protocol/asset_ops.hpp b/libraries/protocol/include/graphene/protocol/asset_ops.hpp index fd4c53297c..0ae800c540 100644 --- a/libraries/protocol/include/graphene/protocol/asset_ops.hpp +++ b/libraries/protocol/include/graphene/protocol/asset_ops.hpp @@ -33,8 +33,9 @@ namespace graphene { namespace protocol { fc::optional reward_percent; fc::optional> whitelist_market_fee_sharing; // After BSIP81 activation, taker_fee_percent is the taker fee - fc::optional taker_fee_percent; + fc::optional taker_fee_percent; }; + typedef extension additional_asset_options_t; bool is_valid_symbol( const string& symbol ); @@ -103,6 +104,8 @@ namespace graphene { namespace protocol { struct ext { + fc::optional margin_call_fee_ratio; // BSIP 74 + fc::optional current_margin_call_fee_ratio; // dynamic based on price feed and margin_call_fee_ratio /// After BSIP77, when creating a new debt position or updating an existing position, /// the position will be checked against this parameter. /// Unused for prediction markets, although we allow it to be set for simpler implementation @@ -563,7 +566,8 @@ FC_REFLECT( graphene::protocol::asset_options, (extensions) ) -FC_REFLECT( graphene::protocol::bitasset_options::ext, (initial_collateral_ratio)(force_settle_fee_percent) ) +FC_REFLECT( graphene::protocol::bitasset_options::ext, + (margin_call_fee_ratio)(current_margin_call_fee_ratio)(initial_collateral_ratio)(force_settle_fee_percent) ) FC_REFLECT( graphene::protocol::bitasset_options, (feed_lifetime_sec) diff --git a/tests/tests/bitasset_tests.cpp b/tests/tests/bitasset_tests.cpp index 797e3a2e66..c10eab5481 100644 --- a/tests/tests/bitasset_tests.cpp +++ b/tests/tests/bitasset_tests.cpp @@ -1407,4 +1407,561 @@ BOOST_AUTO_TEST_CASE(hf_890_test_hf1270) } FC_LOG_AND_RETHROW() } +int num_limit_orders_on_books( graphene::chain::database& db ) +{ + const auto& limit_index = db.get_index_type().indices().get(); + int count = 0; + std::for_each( limit_index.begin(), limit_index.end(), [&count](const limit_order_object& obj) { + count++; + }); + return count; +} + +int num_call_orders_on_books( graphene::chain::database& db ) +{ + const auto& call_index = db.get_index_type().indices().get(); + int count = 0; + std::for_each( call_index.begin(), call_index.end(), [&count](const call_order_object& obj) { + count++; + }); + return count; +} + +void publish_feed_jmjcoin( database_fixture& fixture, const price& price, + const account_object& feeder1, const account_object& feeder2, const account_object& feeder3) +{ + const asset_object& my_asset = price.base.asset_id(fixture.db); + BOOST_TEST_MESSAGE("Create valid feeds"); + price_feed feed1; + feed1.core_exchange_rate = price; + feed1.settlement_price = price; + feed1.maintenance_collateral_ratio = GRAPHENE_DEFAULT_MAINTENANCE_COLLATERAL_RATIO; + feed1.maximum_short_squeeze_ratio = GRAPHENE_DEFAULT_MAX_SHORT_SQUEEZE_RATIO; + fixture.publish_feed( my_asset, feeder1, feed1 ); + fixture.publish_feed( my_asset, feeder2, feed1 ); + fixture.publish_feed( my_asset, feeder3, feed1 ); +} + +asset_id_type create_jmjcoin( database_fixture& fixture, + const account_id_type& owner, const private_key_type& pk, const account_object& feeder1, + const account_object& feeder2, const account_object& feeder3 ) +{ + asset_id_type core_id; + asset_id_type my_asset_id; + // create an asset + { + BOOST_TEST_MESSAGE("Create JMJCOIN"); + asset_create_operation create; + create.issuer = owner; + create.fee = fixture.db.get_global_properties().parameters.current_fees->calculate_fee(create); + create.symbol = "JMJCOIN"; + create.precision = GRAPHENE_BLOCKCHAIN_PRECISION_DIGITS; + create.common_options.market_fee_percent = 10; // 100=1%, 10=0.1% + create.common_options.flags |= charge_market_fee; + create.is_prediction_market = false; + create.bitasset_opts = bitasset_options(); + create.common_options.core_exchange_rate = + graphene::protocol::price( asset(1,asset_id_type(1)), asset(1, core_id)); + fixture.trx.operations.push_back( std::move(create) ); + fixture.sign(fixture.trx, pk); + my_asset_id = PUSH_TX(fixture.db, fixture.trx).operation_results[0].get(); + fixture.trx.clear(); + + BOOST_TEST_MESSAGE("Add 3 feeders"); + flat_set feeders = { feeder1.id, feeder2.id, feeder3.id }; + fixture.update_feed_producers( my_asset_id(fixture.db), feeders ); + } + return my_asset_id; +} + +void adjust_mcfr( database_fixture& fixture, const account_object& owner, const fc::ecc::private_key& pk, + const asset_object& asset, fc::optional ratio) +{ + bitasset_options new_options = asset.bitasset_data(fixture.db).options; + new_options.extensions.value.margin_call_fee_ratio = ratio; + + asset_update_bitasset_operation update; + update.issuer = owner.id; + update.asset_to_update = asset.id; + update.new_options = new_options; + update.fee = fixture.db.get_global_properties().parameters.current_fees->calculate_fee(update); + fixture.trx.operations.push_back( std::move(update) ); + fixture.sign(fixture.trx, pk); + PUSH_TX(fixture.db, fixture.trx); + fixture.trx.clear(); +} + +BOOST_AUTO_TEST_CASE( calculate_margin_fee_test ) +{ + try + { + ACTORS( (charlie) (feeder1) (feeder2) (feeder3) ) + const asset_id_type core_id; + + BOOST_TEST_MESSAGE("Advancing past Hardfork BSIP74"); + generate_blocks( HARDFORK_CORE_BSIP74_TIME + 10); + set_expiration( db, trx ); + + // first create the asset + asset_id_type my_asset_id = create_jmjcoin( *this, charlie_id, charlie_private_key, feeder1, feeder2, feeder3 ); + const asset_object& my_asset = my_asset_id(db); + + // no feeds, no mcfr + BOOST_CHECK_EQUAL( db.calculate_margin_fee(my_asset, asset(100, core_id)).amount.value, 0 ); + // 5% mcfr, but no feed + adjust_mcfr( *this, charlie, charlie_private_key, my_asset, 1050 ); + BOOST_CHECK_EQUAL( db.calculate_margin_fee(my_asset, asset(100, core_id)).amount.value, 0 ); + // add feed (fee still at 5%) + publish_feed_jmjcoin( *this, price( asset(1, my_asset_id), asset(1, core_id) ), feeder1, feeder2, feeder3); + BOOST_CHECK_EQUAL( db.calculate_margin_fee(my_asset, asset(100, core_id)).amount.value, 5 ); + // adjust MCFR to be equal to MSSR + adjust_mcfr( *this, charlie, charlie_private_key, my_asset, 1500 ); + BOOST_CHECK_EQUAL( db.calculate_margin_fee(my_asset, asset(100, core_id)).amount.value, 49 ); + // adjust MCFR to be greater than MSSR (should not change fee) + adjust_mcfr( *this, charlie, charlie_private_key, my_asset, 1600 ); + BOOST_CHECK_EQUAL( db.calculate_margin_fee(my_asset, asset(100, core_id)).amount.value, 49 ); + } + FC_LOG_AND_RETHROW() +} + +/** + * In this test, the limit order is the maker, and the call is the taker. + * Short squeeze price should be based on MSSR and MCFR + * MCFR should be taken from the collateral that the call order receives + */ +BOOST_AUTO_TEST_CASE( bsip74_feed_price_test ) +{ try { + const auto& prec = GRAPHENE_BLOCKCHAIN_PRECISION; + enable_fees(); + const auto fees = db.get_global_properties().parameters.current_fees; + // what is the fee for a limit order? + BOOST_CHECK_EQUAL( 5 * prec, fees->calculate_fee( limit_order_create_operation() ).amount.value); + ACTORS( (alice) (bob) (charlie) (feeder1) (feeder2) (feeder3) ); + auto& core = asset_id_type()(db); + const asset_id_type& core_id = core.id; + // alice will eventually overdo it on margin + transfer( committee_account(db), alice, asset(100000 * prec) ); + // bob will take advantage of alice's situation + transfer( committee_account(db), bob, asset(100000 * prec) ); + // charlie will create an asset and trade it + transfer( committee_account(db), charlie, asset(100000 * prec) ); + // price feeders + transfer( committee_account(db), feeder1, asset(100 * prec) ); + transfer( committee_account(db), feeder2, asset(100 * prec) ); + transfer( committee_account(db), feeder3, asset(100 * prec) ); + + asset_id_type my_asset_id; + asset_object my_asset; + // MCFR (margin call fee ratio) should not be adjustable until HF BSIP74 + { + BOOST_TEST_MESSAGE( "Attempt to create a bitasset with margin call fee before HF (should fail)"); + bitasset_options new_options = bitasset_options(); + new_options.extensions.value.margin_call_fee_ratio = 100; + + asset_create_operation create; + create.issuer = charlie_id; + create.fee = fees->calculate_fee(create); + create.symbol = "JMJCOIN"; + create.precision = GRAPHENE_BLOCKCHAIN_PRECISION_DIGITS; + create.common_options.market_fee_percent = 0.1 * GRAPHENE_100_PERCENT; // 1% + create.common_options.core_exchange_rate = price( asset(1,asset_id_type(1)), asset(1, core_id) ); + create.is_prediction_market = false; + create.bitasset_opts = new_options; + trx.operations.push_back( std::move(create) ); + sign(trx, charlie_private_key); + REQUIRE_EXCEPTION_WITH_TEXT( PUSH_TX(db, trx), "BSIP74" ); + trx.clear(); + + // create the coin without the fee will succeed + my_asset_id = create_jmjcoin( *this, charlie_id, charlie_private_key, feeder1, feeder2, feeder3); + + BOOST_TEST_MESSAGE("Attempt to add margin fee before hardfork (should fail)"); + REQUIRE_EXCEPTION_WITH_TEXT( adjust_mcfr( *this, charlie, charlie_private_key, my_asset_id(db), 1050), + "BSIP74" ); + trx.clear(); + } + + BOOST_TEST_MESSAGE("Advancing past Hardfork BSIP74"); + generate_blocks( HARDFORK_CORE_BSIP74_TIME + 10); + set_expiration( db, trx ); + + BOOST_TEST_MESSAGE("Existing Coins should still not have margin_call_fee_ratio set"); + my_asset = my_asset_id(db); + BOOST_CHECK( !my_asset.bitasset_data(db).options.extensions.value.margin_call_fee_ratio.valid() ); + + { + BOOST_TEST_MESSAGE("BSIP 74 has passed, now update the fee"); + adjust_mcfr( *this, charlie, charlie_private_key, my_asset, 1050 ); + + my_asset = my_asset_id(db); + BOOST_CHECK( my_asset.bitasset_data(db).options.extensions.value.margin_call_fee_ratio.valid() ); + BOOST_CHECK_EQUAL( *my_asset.bitasset_data(db).options.extensions.value.margin_call_fee_ratio, 1050 ); + } + { + BOOST_TEST_MESSAGE("Verify margin fee is zero, as feeds are invalid"); + asset trade_amount(100000, core_id ); + asset fee = db.calculate_margin_fee(my_asset_id(db), trade_amount ); + BOOST_CHECK_EQUAL( fee.amount.value, 0 ); + BOOST_CHECK_EQUAL( fee.asset_id.instance.value, core_id.instance.value); + } + { + BOOST_TEST_MESSAGE("Create valid feeds"); + publish_feed_jmjcoin( *this, price( asset(100, my_asset_id), asset( 100, core_id) ), feeder1, feeder2, feeder3); + } + { + BOOST_TEST_MESSAGE("Verify margin fee is now non-zero"); + asset trade_amount(100000, core_id ); + asset fee = db.calculate_margin_fee(my_asset_id(db), trade_amount ); + BOOST_CHECK_EQUAL( fee.amount.value, 5000 ); + BOOST_CHECK_EQUAL( fee.asset_id.instance.value, core_id.instance.value); + } + { + BOOST_TEST_MESSAGE("Borrow some JMJCOIN into existence"); + borrow( alice, asset(100 * prec, my_asset_id), asset(200 * prec) ); + borrow( charlie, asset( 100 * prec, my_asset_id), asset(300 * prec) ); + } + { + BOOST_TEST_MESSAGE("Sell JMJCOIN to Bob (100-5=95 satoshis"); + create_sell_order( alice_id, asset(95 * prec, my_asset_id), asset(95 * prec, core_id), + fc::time_point_sec::maximum(), + price( asset(100, my_asset_id), asset(100, core_id ) ) ); + create_sell_order( charlie_id, asset(95 * prec, my_asset_id), asset(95 * prec, core_id), + fc::time_point_sec::maximum(), + price( asset(100, my_asset_id), asset(100, core_id ) ) ); + BOOST_TEST_MESSAGE("Bob buys all JMJCOIN on the book"); + create_sell_order( bob_id, asset(190 * prec, core_id), asset(190 * prec, my_asset_id), + fc::time_point_sec::maximum(), price( asset(100, my_asset_id), asset(100, core_id ) ) ); + // now alice holds only core, but has debt in JMJCOIN + BOOST_CHECK_EQUAL( get_balance(alice, my_asset_id(db) ), 0 ); + // and bob holds core and JMJCOIN 19000 market fee + // 19,000,000 received from order, minus 19,000 market fee - 500,000 tx fee = + BOOST_CHECK_EQUAL( get_balance(bob, my_asset_id(db) ), 18481000 ); + } + BOOST_CHECK_EQUAL( 0, num_limit_orders_on_books( db ) ); // bob's order was completely filled + BOOST_CHECK_EQUAL( 2, num_call_orders_on_books( db ) ); // Alice and Charlie's calls still exist + { + BOOST_TEST_MESSAGE("Bob places order to sell JMJCOIN for CORE"); + create_sell_order( bob_id, asset( 100 * prec, my_asset_id ), asset(170 * prec, core_id) ); + BOOST_CHECK_EQUAL( 1, num_limit_orders_on_books( db ) ); + BOOST_CHECK_EQUAL( 2, num_call_orders_on_books( db ) ); // Alice and Charlie's calls still exist + } + auto previous_core_balance_alice = get_balance(alice, core); + auto previous_core_balance_bob = get_balance(bob, core); + auto previous_core_balance_charlie = get_balance(charlie, core); + // now the price of CORE drops, pushing Alice's loan into margin call territory + // at a price of 100:100 (1:1), alice was collateralized at 200% (1:2). + // A drop in price to 100:118 puts her below the 175% threshold. + { + BOOST_TEST_MESSAGE("Value of CORE drops, pushing Alice's loan into margin call territory"); + price_feed feed1; + feed1.core_exchange_rate = price( asset( 100, my_asset_id ), asset( 118, core_id ) ); + //NOTE: settlement_price is what margin calls are based on. + feed1.settlement_price = price( asset( 100, my_asset_id ), asset( 118, core_id ) ); + feed1.maintenance_collateral_ratio = GRAPHENE_DEFAULT_MAINTENANCE_COLLATERAL_RATIO; + publish_feed( my_asset_id(db), feeder1, feed1 ); + publish_feed( my_asset_id(db), feeder2, feed1 ); // <- This one triggers the margin call + publish_feed( my_asset_id(db), feeder3, feed1 ); + BOOST_TEST_MESSAGE( "New Short Squeeze Price is: " + fc::json::to_pretty_string( my_asset.bitasset_data(db) + .current_feed.max_short_squeeze_price())); + } + + // GS should not have happened + BOOST_CHECK( !my_asset_id(db).bitasset_data(db).has_settlement() ); + + // Alice's call should be gone, so should Bob's limit order + BOOST_CHECK_EQUAL(num_call_orders_on_books( db ), 1 ); + BOOST_CHECK_EQUAL(num_limit_orders_on_books( db ), 0 ); + + // the order should have executed, giving Alice what is left of her collateral + // She received 10,000,000 JMJCOIN at 100:170, giving Bob 17,000,000 CORE, plus she paid a 5% (850,000) margin fee + // frees up 2,150,000 CORE + BOOST_CHECK_EQUAL( get_balance(alice, core), previous_core_balance_alice + 2150000 ); + BOOST_CHECK_EQUAL( get_balance(alice, my_asset_id(db) ), 0 ); + // and here are Bob's holdings + BOOST_CHECK_EQUAL( get_balance( bob, core), previous_core_balance_bob + 17000000 ); + // chalie should have collected the margin call fee in the asset's collateral fees + BOOST_CHECK_EQUAL( get_balance( charlie, core), previous_core_balance_charlie ); + BOOST_CHECK_EQUAL( my_asset_id(db).dynamic_data(db).accumulated_collateral_fees.value, 850000 ); + +} FC_LOG_AND_RETHROW() } + +/** + * In this test, the limit order is the taker, and the call is the maker. + * Short squeeze price should only be based on MSSR, and not MCFR + * MCFR should be taken from the collateral that the limit order receives + */ +BOOST_AUTO_TEST_CASE( bsip74_limit_price_test ) +{ try { + const auto& prec = GRAPHENE_BLOCKCHAIN_PRECISION; + enable_fees(); + + generate_blocks( HARDFORK_CORE_BSIP74_TIME + 10); + set_expiration( db, trx ); + + const auto fees = db.get_global_properties().parameters.current_fees; + + ACTORS( (alice) (bob) (charlie) (feeder1) (feeder2) (feeder3) ); + auto& core = asset_id_type()(db); + const asset_id_type& core_id = core.id; + // alice will eventually overdo it on margin + transfer( committee_account(db), alice, asset(100000 * prec) ); + // bob will take advantage of alice's situation + transfer( committee_account(db), bob, asset(100000 * prec) ); + // charlie will create an asset and trade it + transfer( committee_account(db), charlie, asset(100000 * prec) ); + // price feeders + transfer( committee_account(db), feeder1, asset(100 * prec) ); + transfer( committee_account(db), feeder2, asset(100 * prec) ); + transfer( committee_account(db), feeder3, asset(100 * prec) ); + + int64_t expected_core_balance_alice = 100000 * prec; + int64_t expected_core_balance_bob = 100000 * prec; + int64_t expected_core_balance_charlie = 100000 * prec; + + asset_id_type my_asset_id; + asset_object my_asset; + { + my_asset_id = create_jmjcoin( *this, charlie_id, charlie_private_key, feeder1, feeder2, feeder3); + my_asset = my_asset_id(db); + + BOOST_TEST_MESSAGE("BSIP 74 has passed, update the fee to 5%"); + adjust_mcfr( *this, charlie, charlie_private_key, my_asset, 1050); + my_asset = my_asset_id(db); + BOOST_CHECK( my_asset.bitasset_data(db).options.extensions.value.margin_call_fee_ratio.valid() ); + BOOST_CHECK_EQUAL( *my_asset.bitasset_data(db).options.extensions.value.margin_call_fee_ratio, 1050 ); + } + { + BOOST_TEST_MESSAGE("Verify margin fee is zero, as feeds are invalid"); + asset trade_amount(100000, core_id ); + asset fee = db.calculate_margin_fee(my_asset_id(db), trade_amount ); + BOOST_CHECK_EQUAL( fee.amount.value, 0 ); + BOOST_CHECK_EQUAL( fee.asset_id.instance.value, core_id.instance.value); + } + { + BOOST_TEST_MESSAGE("Create valid feeds"); + publish_feed_jmjcoin( *this, price( asset(100, my_asset_id), asset(100, core_id) ), feeder1, feeder2, feeder3); + BOOST_TEST_MESSAGE( "Starting MSSR is: " + fc::json::to_pretty_string( my_asset.bitasset_data(db) + .current_feed.max_short_squeeze_price())); + } + { + BOOST_TEST_MESSAGE("Borrow some JMJCOIN into existence"); + borrow( alice, asset(100 * prec, my_asset_id), asset(200 * prec) ); + borrow( charlie, asset( 100 * prec, my_asset_id), asset(300 * prec) ); + } + { + BOOST_TEST_MESSAGE("Sell JMJCOIN to Bob (100-5=95 satoshis"); + create_sell_order( alice_id, asset(95 * prec, my_asset_id), asset(95 * prec, core_id), + fc::time_point_sec::maximum(), + price( asset(100, my_asset_id), asset(100, core_id ) ) ); + expected_core_balance_alice += 95 * prec; // 95 sold + 5 JMJCoin fee. + create_sell_order( charlie_id, asset(95 * prec, my_asset_id), asset(95 * prec, core_id), + fc::time_point_sec::maximum(), + price( asset(100, my_asset_id), asset(100, core_id ) ) ); + expected_core_balance_charlie += 95 * prec; // 95 sold gives charlie 95 core + BOOST_TEST_MESSAGE("Bob buys all JMJCOIN on the book"); + create_sell_order( bob_id, asset(190*prec, core_id), asset(190*prec, my_asset_id), fc::time_point_sec::maximum(), + price( asset(100, my_asset_id), asset(100, core_id ) ) ); + expected_core_balance_bob -= 190 * prec; // 190 JMJCoin bought for 190 CORE + } + BOOST_CHECK_EQUAL( 0, num_limit_orders_on_books( db ) ); + BOOST_CHECK_EQUAL( 2, num_call_orders_on_books( db ) ); + { + BOOST_TEST_MESSAGE("Adjust feed price close to (but not beyond) where Alice will get margin called"); + price new_price( asset( 100, my_asset_id), asset(116, core_id) ); + publish_feed_jmjcoin( *this, new_price, feeder1, feeder2, feeder3 ); + my_asset = my_asset_id(db); + BOOST_TEST_MESSAGE( "New Short Squeeze Price is: " + fc::json::to_pretty_string( my_asset.bitasset_data(db) + .current_feed.max_short_squeeze_price())); + } + // gather balances to watch fees + auto bob_jmjcoin_balance = db.get_balance(bob_id, my_asset_id).amount.value; + auto bob_core_balance = db.get_balance(bob_id, core_id).amount.value; + auto alice_core_balance = db.get_balance(alice_id, core_id).amount.value; + { + BOOST_TEST_MESSAGE("Bob places order to sell JMJCOIN for CORE below max short squeeze price"); + create_sell_order( bob_id, asset( 100 * prec, my_asset_id ), asset(174 * prec, core_id) ); + // Bob's order should have executed + BOOST_CHECK_EQUAL( 0, num_limit_orders_on_books( db ) ); + // only Charlie's call order is left + BOOST_CHECK_EQUAL( 1, num_call_orders_on_books( db ) ); + // Bob should have sold 100 jmjcoin + BOOST_CHECK_EQUAL( db.get_balance(bob_id, my_asset_id).amount.value, bob_jmjcoin_balance - (100*prec)); + // the match price was based on Bob's limit order price which was [100:174], + // so 10,000,000 JMJCOIN receives 17,400,000 core + // minus transaction fee (500,000) and margin call fee (5%=870,000) + auto limit_order_fee = fees->calculate_fee(limit_order_create_operation()).amount.value; + BOOST_CHECK_EQUAL( db.get_balance(bob_id, core_id).amount.value, + bob_core_balance + 17400000 - limit_order_fee - 870000 ); + // Alice should have no JMJCOIN + BOOST_CHECK_EQUAL( db.get_balance(alice_id, my_asset_id).amount.value, 0); + // Alice should have gained what is left of her collateral back. + // she originally put up 20,000,000, but Bob took 17,400,000, so she is freed from debt, but only gets + // 2,600,000 of her collateral back + BOOST_CHECK_EQUAL( db.get_balance(alice_id, core_id).amount.value, alice_core_balance + 2600000 ); + } + +} FC_LOG_AND_RETHROW() } + +/** + * In this test, the limit order is the maker, and the call is the taker. + * MCFR should be taken from the collateral that the call order receives, + * but there is not enough collateral to do so. + */ +BOOST_AUTO_TEST_CASE( bsip74_insufficient_collateral_test ) +{ try { + const auto& prec = GRAPHENE_BLOCKCHAIN_PRECISION; + enable_fees(); + const auto fees = db.get_global_properties().parameters.current_fees; + // what is the fee for a limit order? + BOOST_CHECK_EQUAL( 5 * prec, fees->calculate_fee( limit_order_create_operation() ).amount.value); + ACTORS( (alice) (bob) (charlie) (feeder1) (feeder2) (feeder3) ); + auto& core = asset_id_type()(db); + const asset_id_type& core_id = core.id; + // alice will eventually overdo it on margin + transfer( committee_account(db), alice, asset(100000 * prec) ); + // bob will take advantage of alice's situation + transfer( committee_account(db), bob, asset(100000 * prec) ); + // charlie will create an asset and trade it + transfer( committee_account(db), charlie, asset(100000 * prec) ); + // price feeders + transfer( committee_account(db), feeder1, asset(100 * prec) ); + transfer( committee_account(db), feeder2, asset(100 * prec) ); + transfer( committee_account(db), feeder3, asset(100 * prec) ); + + asset_id_type my_asset_id; + asset_object my_asset; + // MCFR (margin call fee ratio) should not be adjustable until HF BSIP74 + { + BOOST_TEST_MESSAGE( "Attempt to create a bitasset with margin call fee before HF (should fail)"); + bitasset_options new_options = bitasset_options(); + new_options.extensions.value.margin_call_fee_ratio = 1050; + + asset_create_operation create; + create.issuer = charlie_id; + create.fee = fees->calculate_fee(create); + create.symbol = "JMJCOIN"; + create.precision = GRAPHENE_BLOCKCHAIN_PRECISION_DIGITS; + create.common_options.market_fee_percent = 0.1 * GRAPHENE_100_PERCENT; // 1% + create.common_options.core_exchange_rate = price( asset(1,asset_id_type(1)), asset(1, core_id) ); + create.is_prediction_market = false; + create.bitasset_opts = new_options; + trx.operations.push_back( std::move(create) ); + sign(trx, charlie_private_key); + REQUIRE_EXCEPTION_WITH_TEXT( PUSH_TX(db, trx), "BSIP74" ); + trx.clear(); + + // create the coin without the fee will succeed + my_asset_id = create_jmjcoin( *this, charlie_id, charlie_private_key, feeder1, feeder2, feeder3); + + BOOST_TEST_MESSAGE("Attempt to add margin fee before hardfork (should fail)"); + REQUIRE_EXCEPTION_WITH_TEXT( adjust_mcfr( *this, charlie, charlie_private_key, my_asset_id(db), 1050), + "BSIP74" ); + trx.clear(); + } + + BOOST_TEST_MESSAGE("Advancing past Hardfork BSIP74"); + generate_blocks( HARDFORK_CORE_BSIP74_TIME + 10); + set_expiration( db, trx ); + + BOOST_TEST_MESSAGE("Existing Coins should still not have margin_call_fee_ratio set"); + my_asset = my_asset_id(db); + BOOST_CHECK( !my_asset.bitasset_data(db).options.extensions.value.margin_call_fee_ratio.valid() ); + + { + BOOST_TEST_MESSAGE("BSIP 74 has passed, now update the fee"); + adjust_mcfr( *this, charlie, charlie_private_key, my_asset, 1050 ); + + my_asset = my_asset_id(db); + BOOST_CHECK( my_asset.bitasset_data(db).options.extensions.value.margin_call_fee_ratio.valid() ); + BOOST_CHECK_EQUAL( *my_asset.bitasset_data(db).options.extensions.value.margin_call_fee_ratio, 1050 ); + } + { + BOOST_TEST_MESSAGE("Verify margin fee is zero, as feeds are invalid"); + asset trade_amount(100000, core_id ); + asset fee = db.calculate_margin_fee(my_asset_id(db), trade_amount ); + BOOST_CHECK_EQUAL( fee.amount.value, 0 ); + BOOST_CHECK_EQUAL( fee.asset_id.instance.value, core_id.instance.value); + } + { + BOOST_TEST_MESSAGE("Create valid feeds"); + publish_feed_jmjcoin( *this, price( asset(100, my_asset_id), asset( 100, core_id) ), feeder1, feeder2, feeder3); + } + { + BOOST_TEST_MESSAGE("Verify margin fee is now non-zero"); + asset trade_amount(100000, core_id ); + asset fee = db.calculate_margin_fee(my_asset_id(db), trade_amount ); + BOOST_CHECK_EQUAL( fee.amount.value, 5000 ); + BOOST_CHECK_EQUAL( fee.asset_id.instance.value, core_id.instance.value); + } + { + BOOST_TEST_MESSAGE("Borrow some JMJCOIN into existence"); + borrow( alice, asset(100 * prec, my_asset_id), asset(200 * prec) ); + borrow( charlie, asset( 100 * prec, my_asset_id), asset(300 * prec) ); + } + { + BOOST_TEST_MESSAGE("Sell JMJCOIN to Bob (100-5=95 satoshis"); + create_sell_order( alice_id, asset(95 * prec, my_asset_id), asset(95 * prec, core_id), + fc::time_point_sec::maximum(), + price( asset(100, my_asset_id), asset(100, core_id ) ) ); + create_sell_order( charlie_id, asset(95 * prec, my_asset_id), asset(95 * prec, core_id), + fc::time_point_sec::maximum(), + price( asset(100, my_asset_id), asset(100, core_id ) ) ); + BOOST_TEST_MESSAGE("Bob buys all JMJCOIN on the book"); + create_sell_order( bob_id, asset(190*prec, core_id), asset(190*prec, my_asset_id), fc::time_point_sec::maximum(), + price( asset(100, my_asset_id), asset(100, core_id ) ) ); + // now alice holds only core, but has debt in JMJCOIN + BOOST_CHECK_EQUAL( get_balance(alice, my_asset_id(db) ), 0 ); + // and bob holds core and JMJCOIN 19000 market fee + // 19,000,000 received from order, minus 19,000 market fee - 500,000 tx fee = + BOOST_CHECK_EQUAL( get_balance(bob, my_asset_id(db) ), 18481000 ); + } + BOOST_CHECK_EQUAL( 0, num_limit_orders_on_books( db ) ); // bob's order was completely filled + BOOST_CHECK_EQUAL( 2, num_call_orders_on_books( db ) ); // Alice and Charlie's calls still exist + { + BOOST_TEST_MESSAGE("Bob places order to sell JMJCOIN for CORE"); + create_sell_order( bob_id, asset( 100 * prec, my_asset_id ), asset(195 * prec, core_id) ); + BOOST_CHECK_EQUAL( 1, num_limit_orders_on_books( db ) ); + BOOST_CHECK_EQUAL( 2, num_call_orders_on_books( db ) ); // Alice and Charlie's calls still exist + } + auto previous_core_balance_alice = get_balance(alice, core); + auto previous_core_balance_bob = get_balance(bob, core); + auto previous_core_balance_charlie = get_balance(charlie, core); + // now the price of CORE drops, pushing Alice's loan into margin call territory + // at a price of 100:100 (1:1), alice was collateralized at 200% (1:2). + // A drop in price to 100:196 puts her below the 175% threshold. + { + BOOST_TEST_MESSAGE("Value of CORE drops, pushing Alice's loan into margin call territory"); + price_feed feed1; + feed1.core_exchange_rate = price( asset( 100, my_asset_id ), asset( 196, core_id ) ); + //NOTE: settlement_price is what margin calls are based on. + feed1.settlement_price = price( asset( 100, my_asset_id ), asset( 196, core_id ) ); + feed1.maintenance_collateral_ratio = GRAPHENE_DEFAULT_MAINTENANCE_COLLATERAL_RATIO; + publish_feed( my_asset_id(db), feeder1, feed1 ); + publish_feed( my_asset_id(db), feeder2, feed1 ); // <- This one triggers the margin call + publish_feed( my_asset_id(db), feeder3, feed1 ); + BOOST_TEST_MESSAGE( "New Short Squeeze Price is: " + fc::json::to_pretty_string( my_asset.bitasset_data(db) + .current_feed.max_short_squeeze_price())); + } + + // GS should not have happened + BOOST_CHECK( !my_asset_id(db).bitasset_data(db).has_settlement() ); + + // Alice's call should be gone, so should Bob's limit order + BOOST_CHECK_EQUAL(num_call_orders_on_books( db ), 1 ); + BOOST_CHECK_EQUAL(num_limit_orders_on_books( db ), 0 ); + + // the order should have executed, giving Alice what is left of her collateral + // She received 10,000,000 JMJCOIN at 100:195, giving Bob 19,500,000 CORE, plus she paid a 5% (975,000) margin fee + // that is 475,000 CORE more collateral than she had in the call order. + BOOST_TEST_MESSAGE("Alice lost all her collateral and then some. This should not happen."); + BOOST_CHECK_GT( get_balance(alice, core), previous_core_balance_alice); + BOOST_CHECK_EQUAL( get_balance(alice, my_asset_id(db) ), 0 ); + // and here are Bob's holdings + BOOST_CHECK_EQUAL( get_balance( bob, core), previous_core_balance_bob + 19500000 ); + // chalie should have collected the margin call fee in the asset's collateral fees + BOOST_CHECK_EQUAL( get_balance( charlie, core), previous_core_balance_charlie ); + BOOST_CHECK_EQUAL( my_asset_id(db).dynamic_data(db).accumulated_collateral_fees.value, 975000 ); + +} FC_LOG_AND_RETHROW() } + BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/tests/swan_tests.cpp b/tests/tests/swan_tests.cpp index e99c0607fb..bf0ed4334e 100644 --- a/tests/tests/swan_tests.cpp +++ b/tests/tests/swan_tests.cpp @@ -224,7 +224,6 @@ BOOST_AUTO_TEST_CASE( black_swan_issue_346 ) price_feed feed; feed.settlement_price = settlement_price; feed.core_exchange_rate = settlement_price; - wdump( (feed.max_short_squeeze_price()) ); publish_feed( bitusd, feeder, feed ); };