diff --git a/libraries/chain/asset_evaluator.cpp b/libraries/chain/asset_evaluator.cpp index e8a5fda0..faa8ce7c 100644 --- a/libraries/chain/asset_evaluator.cpp +++ b/libraries/chain/asset_evaluator.cpp @@ -685,7 +685,7 @@ void_result asset_global_settle_evaluator::do_evaluate(const asset_global_settle FC_ASSERT(asset_to_settle->dynamic_data(d).current_supply > 0); const asset_bitasset_data_object* bitasset_data = &asset_to_settle->bitasset_data(d); - if( bitasset.is_prediction_market ) { + if( bitasset_data.is_prediction_market ) { /// if there is a settlement for this asset, then no further global settle may be taken and FC_ASSERT( !bitasset_data->has_settlement(),"This asset has settlement, cannot global settle twice" ); } diff --git a/libraries/chain/db_market.cpp b/libraries/chain/db_market.cpp index c4a265e5..80466a0c 100644 --- a/libraries/chain/db_market.cpp +++ b/libraries/chain/db_market.cpp @@ -61,24 +61,28 @@ void database::globally_settle_asset( const asset_object& mia, const price& sett const call_order_index& call_index = get_index_type(); const auto& call_price_index = call_index.indices().get(); + auto maint_time = get_dynamic_global_properties().next_maintenance_time; + bool before_core_hardfork_342 = ( maint_time <= HARDFORK_CORE_342_TIME ); // better rounding + // cancel all call orders and accumulate it into collateral_gathered auto call_itr = call_price_index.lower_bound( price::min( bitasset.options.short_backing_asset, mia.id ) ); auto call_end = call_price_index.upper_bound( price::max( bitasset.options.short_backing_asset, mia.id ) ); + asset pays; while( call_itr != call_end ) { - auto pays = call_itr->get_debt() * settlement_price; // round down, in favor of call order + if( before_core_hardfork_342 ) + { + pays = call_itr->get_debt() * settlement_price; // round down, in favor of call order + + // Be here, the call order can be paying nothing + if( pays.amount == 0 && !bitasset.is_prediction_market ) // TODO remove this warning after hard fork core-342 + wlog( "Something for nothing issue (#184, variant E) occurred at block #${block}", ("block",head_block_num()) ); + } + else + pays = call_itr->get_debt() ^ settlement_price; // round up, in favor of global settlement fund if( pays > call_itr->get_collateral() ) pays = call_itr->get_collateral(); - - // Be here, the call order can be paying nothing, in this case, we take at least 1 Satoshi from call order - if( pays.amount == 0 && !bitasset.is_prediction_market ) - { - if( get_dynamic_global_properties().next_maintenance_time > HARDFORK_CORE_184_TIME ) - pays.amount = 1; - else // TODO remove this warning after hard fork core-184 - wlog( "Something for nothing issue (#184, variant E) occurred at block #${block}", ("block",head_block_num()) ); - } collateral_gathered += pays; const auto& order = *call_itr; @@ -382,16 +386,23 @@ bool database::apply_order(const limit_order_object& new_order_object, bool allo // check if there are margin calls const auto& call_price_idx = get_index_type().indices().get(); auto call_min = price::min( recv_asset_id, sell_asset_id ); - auto call_itr = call_price_idx.lower_bound( call_min ); - // feed protected https://github.com/cryptonomex/graphene/issues/436 - auto call_end = call_price_idx.upper_bound( ~sell_abd->current_feed.settlement_price ); - while( !finished && call_itr != call_end ) + while( !finished ) { - auto old_call_itr = call_itr; - ++call_itr; // would be safe, since we'll end the loop if a call order is partially matched - // match returns 2 when only the old order was fully filled. In this case, we keep matching; otherwise, we stop. + // assume hard fork core-343 and core-625 will take place at same time, always check call order with least call_price + auto call_itr = call_price_idx.lower_bound( call_min ); + if( call_itr == call_price_idx.end() + || call_itr->debt_type() != sell_asset_id + // feed protected https://github.com/cryptonomex/graphene/issues/436 + || call_itr->call_price > ~sell_abd->current_feed.settlement_price ) + break; // assume hard fork core-338 and core-625 will take place at same time, not checking HARDFORK_CORE_338_TIME here. - finished = ( match( new_order_object, *old_call_itr, call_match_price ) != 2 ); + int match_result = match( new_order_object, *call_itr, call_match_price, + sell_abd->current_feed.settlement_price, + sell_abd->current_feed.maintenance_collateral_ratio ); + // match returns 1 or 3 when the new order was fully filled. In this case, we stop matching; otherwise keep matching. + // since match can return 0 due to BSIP38 (hard fork core-834), we no longer only check if the result is 2. + if( match_result == 1 || match_result == 3 ) + finished = true; } } } @@ -428,19 +439,39 @@ bool database::apply_order(const limit_order_object& new_order_object, bool allo */ int database::match( const limit_order_object& usd, const limit_order_object& core, const price& match_price ) { - assert( usd.sell_price.quote.asset_id == core.sell_price.base.asset_id ); - assert( usd.sell_price.base.asset_id == core.sell_price.quote.asset_id ); - assert( usd.for_sale > 0 && core.for_sale > 0 ); + FC_ASSERT( usd.sell_price.quote.asset_id == core.sell_price.base.asset_id ); + FC_ASSERT( usd.sell_price.base.asset_id == core.sell_price.quote.asset_id ); + FC_ASSERT( usd.for_sale > 0 && core.for_sale > 0 ); auto usd_for_sale = usd.amount_for_sale(); auto core_for_sale = core.amount_for_sale(); asset usd_pays, usd_receives, core_pays, core_receives; - if( usd_for_sale <= core_for_sale * match_price ) + auto maint_time = get_dynamic_global_properties().next_maintenance_time; + bool before_core_hardfork_342 = ( maint_time <= HARDFORK_CORE_342_TIME ); // better rounding + + bool cull_taker = false; + if( usd_for_sale <= core_for_sale * match_price ) // rounding down here should be fine { - core_receives = usd_for_sale; usd_receives = usd_for_sale * match_price; // round down, in favor of bigger order + + // Be here, it's possible that taker is paying something for nothing due to partially filled in last loop. + // In this case, we see it as filled and cancel it later + if( usd_receives.amount == 0 && maint_time > HARDFORK_CORE_184_TIME ) + return 1; + + if( before_core_hardfork_342 ) + core_receives = usd_for_sale; + else + { + // The remaining amount in order `usd` would be too small, + // 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. + core_receives = usd_receives ^ match_price; + cull_taker = true; + } } else { @@ -448,85 +479,102 @@ int database::match( const limit_order_object& usd, const limit_order_object& co //This assert is not always true -- see trade_amount_equals_zero in operation_tests.cpp //Although usd_for_sale is greater than core_for_sale * match_price, core_for_sale == usd_for_sale * match_price //Removing the assert seems to be safe -- apparently no asset is created or destroyed. - usd_receives = core_for_sale; + // The maker won't be paying something for nothing, since if it would, it would have been cancelled already. core_receives = core_for_sale * match_price; // round down, in favor of bigger order + if( before_core_hardfork_342 ) + usd_receives = core_for_sale; + else + // The remaining amount in order `core` would be too small, + // so the order will be culled in fill_limit_order() below + usd_receives = core_receives ^ match_price; } core_pays = usd_receives; usd_pays = core_receives; - assert( usd_pays == usd.amount_for_sale() || - core_pays == core.amount_for_sale() ); - - // Be here, it's possible that taker is paying something for nothing due to partially filled in last loop. - // In this case, we see it as filled and cancel it later - // The maker won't be paying something for nothing, since if it would, it would have been cancelled already. - if( usd_receives.amount == 0 && get_dynamic_global_properties().next_maintenance_time > HARDFORK_CORE_184_TIME ) - return 1; + if( before_core_hardfork_342 ) + FC_ASSERT( usd_pays == usd.amount_for_sale() || + core_pays == core.amount_for_sale() ); int result = 0; - result |= fill_limit_order( usd, usd_pays, usd_receives, false, match_price, false ); // the first param is taker + result |= fill_limit_order( usd, usd_pays, usd_receives, cull_taker, match_price, false ); // the first param is taker result |= fill_limit_order( core, core_pays, core_receives, true, match_price, true ) << 1; // the second param is maker - assert( result != 0 ); + FC_ASSERT( result != 0 ); return result; } -int database::match( const limit_order_object& bid, const call_order_object& ask, const price& match_price ) +int database::match( const limit_order_object& bid, const call_order_object& ask, const price& match_price, + const price& feed_price, const uint16_t maintenance_collateral_ratio ) { FC_ASSERT( bid.sell_asset_id() == ask.debt_type() ); FC_ASSERT( bid.receive_asset_id() == ask.collateral_type() ); FC_ASSERT( bid.for_sale > 0 && ask.debt > 0 && ask.collateral > 0 ); - bool filled_limit = false; - bool filled_call = false; + auto maint_time = get_dynamic_global_properties().next_maintenance_time; + // TODO remove when we're sure it's always false + bool before_core_hardfork_184 = ( maint_time <= HARDFORK_CORE_184_TIME ); // something-for-nothing + bool before_core_hardfork_342 = ( maint_time <= HARDFORK_CORE_342_TIME ); // better rounding + if( before_core_hardfork_184 ) + ilog( "match(limit,call) is called before hardfork core-184 at block #${block}", ("block",head_block_num()) ); + if( before_core_hardfork_342 ) + ilog( "match(limit,call) is called before hardfork core-342 at block #${block}", ("block",head_block_num()) ); + + // TODO remove when we're sure it's always false + bool before_core_hardfork_834 = ( maint_time <= HARDFORK_CORE_834_TIME ); // target collateral ratio option + + bool cull_taker = false; asset usd_for_sale = bid.amount_for_sale(); - asset usd_to_buy = ask.get_debt(); + // TODO if we're sure `before_core_hardfork_834` is always false, remove the check + asset usd_to_buy = ( before_core_hardfork_834 ? + ask.get_debt() : + asset( ask.get_max_debt_to_cover( match_price, feed_price, maintenance_collateral_ratio ), + ask.debt_type() ) ); asset call_pays, call_receives, order_pays, order_receives; - if( usd_to_buy >= usd_for_sale ) + if( usd_to_buy > usd_for_sale ) { // fill limit order - call_receives = usd_for_sale; order_receives = usd_for_sale * match_price; // round down here, in favor of call order - call_pays = order_receives; - order_pays = usd_for_sale; - filled_limit = true; - filled_call = ( usd_to_buy == usd_for_sale ); + // Be here, it's possible that taker is paying something for nothing due to partially filled in last loop. + // In this case, we see it as filled and cancel it later + // TODO remove hardfork check when we're sure it's always true (but keep the zero amount check) + if( order_receives.amount == 0 && !before_core_hardfork_184 ) + return 1; + + if( before_core_hardfork_342 ) // TODO remove this "if" when we're sure it's always false (keep the code in else) + call_receives = usd_for_sale; + else + { + // The remaining amount in the limit order would be too small, + // 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 ^ match_price; + cull_taker = true; + } } else { // fill call order call_receives = usd_to_buy; - order_receives = usd_to_buy * match_price; // round down here, in favor of call order - call_pays = order_receives; - order_pays = usd_to_buy; - - filled_call = true; - } - - FC_ASSERT( filled_call || filled_limit ); - - // Be here, it's possible that taker is paying something for nothing. - // The maker won't be paying something for nothing according to code above - if( order_receives.amount == 0 && get_dynamic_global_properties().next_maintenance_time > HARDFORK_CORE_184_TIME ) - { - // It's possible that taker is paying something for nothing due to call order too small. - // In this case, let the call pay 1 Satoshi - if( filled_call ) + if( before_core_hardfork_342 ) // TODO remove this "if" when we're sure it's always false (keep the code in else) { - order_receives.amount = 1; - call_pays = order_receives; + order_receives = usd_to_buy * match_price; // round down here, in favor of call order + // TODO remove hardfork check when we're sure it's always true (but keep the zero amount check) + if( order_receives.amount == 0 && !before_core_hardfork_184 ) + return 1; } - else - // It's possible that taker is paying something for nothing due to partially filled in last loop. - // In this case, we see it as filled and cancel it later - return 1; + else // has hardfork core-342 + order_receives = usd_to_buy ^ match_price; // round up here, in favor of limit order } + call_pays = order_receives; + order_pays = call_receives; + int result = 0; - result |= fill_limit_order( bid, order_pays, order_receives, false, match_price, false ); // the limit order is taker + 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 - FC_ASSERT( result != 0 ); + // result can be 0 when call order has target_collateral_ratio option set. return result; } @@ -540,16 +588,21 @@ asset database::match( const call_order_object& call, FC_ASSERT(call.get_debt().asset_id == settle.balance.asset_id ); FC_ASSERT(call.debt > 0 && call.collateral > 0 && settle.balance.amount > 0); + auto maint_time = get_dynamic_global_properties().next_maintenance_time; + bool before_core_hardfork_342 = ( maint_time <= HARDFORK_CORE_342_TIME ); // better rounding + auto settle_for_sale = std::min(settle.balance, max_settlement); auto call_debt = call.get_debt(); asset call_receives = std::min(settle_for_sale, call_debt); - asset call_pays = call_receives * match_price; // round down here, in favor of call order + asset call_pays = call_receives * match_price; // round down here, in favor of call order, for first check + // TODO possible optimization: check need to round up or down first // Be here, the call order may be paying nothing. + bool cull_settle_order = false; // whether need to cancel dust settle order if( call_pays.amount == 0 ) { - if( get_dynamic_global_properties().next_maintenance_time > HARDFORK_CORE_184_TIME ) + if( maint_time > HARDFORK_CORE_184_TIME ) { if( call_receives == call_debt ) // the call order is smaller than or equal to the settle order { @@ -563,14 +616,40 @@ asset database::match( const call_order_object& call, wlog( "Something for nothing issue (#184, variant C-2) handled at block #${block}", ("block",head_block_num()) ); cancel_settle_order( settle ); } - else // neither order will be completely filled, perhaps due to max_settlement too small - wlog( "Something for nothing issue (#184, variant C-3) handled at block #${block}", ("block",head_block_num()) ); + // else do nothing: neither order will be completely filled, perhaps due to max_settlement too small return asset( 0, settle.balance.asset_id ); } } else wlog( "Something for nothing issue (#184, variant C) occurred at block #${block}", ("block",head_block_num()) ); } + else // the call order is not paying nothing, but still possible it's paying more than minimum required due to rounding + { + if( !before_core_hardfork_342 ) + { + if( call_receives == call_debt ) // the call order is smaller than or equal to the settle order + { + call_pays = call_receives ^ match_price; // round up here, in favor of settle order + // be here, we should have: call_pays <= call_collateral + } + else + { + // be here, call_pays has been rounded down + + // be here, we should have: call_pays <= call_collateral + + if( call_receives == settle.balance ) // the settle order will be completely filled, assuming we need to cull it + cull_settle_order = true; + // else do nothing, since we can't cull the settle order + + call_receives = call_pays ^ match_price; // round up here to mitigate rouding issue (core-342) + + if( call_receives == settle.balance ) // the settle order will be completely filled, no need to cull + cull_settle_order = false; + // else do nothing, since we still need to cull the settle order or still can't cull the settle order + } + } + } asset settle_pays = call_receives; asset settle_receives = call_pays; @@ -582,13 +661,23 @@ asset database::match( const call_order_object& call, * can trigger a black swan. So now we must cancel the forced settlement * object. */ - GRAPHENE_ASSERT( call_pays < call.get_collateral(), black_swan_exception, "" ); + if( before_core_hardfork_342 ) + { + auto call_collateral = call.get_collateral(); + if( call_pays == call_collateral ) + wlog( "Incorrectly captured black swan event at block #${block}", ("block",head_block_num()) ); + GRAPHENE_ASSERT( call_pays < call_collateral, black_swan_exception, "" ); - assert( settle_pays == settle_for_sale || call_receives == call.get_debt() ); + assert( settle_pays == settle_for_sale || call_receives == call.get_debt() ); + } + // else do nothing, since black swan event won't happen, and the assertion is no longer true fill_call_order( call, call_pays, call_receives, fill_price, true ); // call order is maker fill_settle_order( settle, settle_pays, settle_receives, fill_price, false ); // force settlement order is taker + if( cull_settle_order ) + cancel_settle_order( settle ); + return call_receives; } FC_CAPTURE_AND_RETHROW( (call)(settle)(match_price)(max_settlement) ) } @@ -678,14 +767,14 @@ bool database::fill_call_order( const call_order_object& order, const asset& pay }); const account_object& borrower = order.borrower(*this); - if( collateral_freed || pays.asset_id == asset_id_type() ) + if( collateral_freed.valid() || pays.asset_id == asset_id_type() ) { const account_statistics_object& borrower_statistics = borrower.statistics(*this); - if( collateral_freed ) + if( collateral_freed.valid() ) adjust_balance(borrower.get_id(), *collateral_freed); modify( borrower_statistics, [&]( account_statistics_object& b ){ - if( collateral_freed && collateral_freed->amount > 0 && collateral_freed->asset_id == asset_id_type()) + if( collateral_freed.valid() && collateral_freed->amount > 0 && collateral_freed->asset_id == asset_id_type()) b.total_core_in_orders -= collateral_freed->amount; if( pays.asset_id == asset_id_type() ) b.total_core_in_orders -= pays.amount; @@ -698,7 +787,7 @@ bool database::fill_call_order( const call_order_object& order, const asset& pay push_applied_operation( fill_order_operation( order.id, order.borrower, pays, receives, asset(0, pays.asset_id), fill_price, is_maker ) ); - if( collateral_freed ) + if( collateral_freed.valid() ) remove( order ); return collateral_freed.valid(); @@ -746,6 +835,11 @@ bool database::fill_settle_order( const force_settlement_object& settle, const a bool database::check_call_orders( const asset_object& mia, bool enable_black_swan, bool for_new_limit_order, const asset_bitasset_data_object* bitasset_ptr ) { try { + auto head_time = head_block_time(); + auto maint_time = get_dynamic_global_properties().next_maintenance_time; + if( for_new_limit_order ) + FC_ASSERT( maint_time <= HARDFORK_CORE_625_TIME ); // `for_new_limit_order` is only true before HF 338 / 625 + if( !mia.is_market_issued() ) return false; const asset_bitasset_data_object& bitasset = ( bitasset_ptr ? *bitasset_ptr : mia.bitasset_data(*this) ); @@ -780,19 +874,20 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa auto call_itr = call_price_index.lower_bound( call_min ); auto call_end = call_price_index.upper_bound( call_max ); - bool filled_limit = false; - bool margin_called = false; - auto head_time = head_block_time(); auto head_num = head_block_num(); bool after_hardfork_436 = ( head_time > HARDFORK_436_TIME ); - auto head_time = head_block_time(); - auto maint_time = get_dynamic_global_properties().next_maintenance_time; + bool before_core_hardfork_184 = ( maint_time <= HARDFORK_CORE_184_TIME ); // something-for-nothing + bool before_core_hardfork_342 = ( maint_time <= HARDFORK_CORE_342_TIME ); // better rounding + bool before_core_hardfork_343 = ( maint_time <= HARDFORK_CORE_343_TIME ); // update call_price after partially filled + bool before_core_hardfork_453 = ( maint_time <= HARDFORK_CORE_453_TIME ); // multiple matching issue + bool before_core_hardfork_606 = ( maint_time <= HARDFORK_CORE_606_TIME ); // feed always trigger call + bool before_core_hardfork_834 = ( maint_time <= HARDFORK_CORE_834_TIME ); // target collateral ratio option + while( !check_for_blackswan( mia, enable_black_swan, &bitasset ) && call_itr != call_end ) { - bool filled_limit_in_loop = false; bool filled_call = false; price match_price; asset usd_for_sale; @@ -807,12 +902,11 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa match_price.validate(); // Feed protected (don't call if CR>MCR) https://github.com/cryptonomex/graphene/issues/436 - if( ( head_time > HARDFORK_436_TIME ) - && ( bitasset.current_feed.settlement_price > ~call_itr->call_price ) ) + if( after_hardfork_436 && ( bitasset.current_feed.settlement_price > ~call_itr->call_price ) ) return margin_called; // Old rule: margin calls can only buy high https://github.com/bitshares/bitshares-core/issues/606 - if( maint_time <= HARDFORK_CORE_606_TIME && match_price > ~call_itr->call_price ) + if( before_core_hardfork_606 && match_price > ~call_itr->call_price ) return margin_called; margin_called = true; @@ -822,75 +916,82 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa if( usd_to_buy * match_price > call_itr->get_collateral() ) { elog( "black swan detected on asset ${symbol} (${id}) at block ${b}", - ("id",mia.id)("symbol",mia.symbol)("b",head_num) ); + ("id",mia.id)("symbol",mia.symbol)("b",head_block_num()) ); edump((enable_black_swan)); FC_ASSERT( enable_black_swan ); globally_settle_asset(mia, bitasset.current_feed.settlement_price ); return true; } - asset call_pays, call_receives, order_pays, order_receives; - if( usd_to_buy >= usd_for_sale ) - { // fill order - call_receives = usd_for_sale; - order_receives = usd_for_sale * match_price; // round down, in favor of call order - call_pays = order_receives; - order_pays = usd_for_sale; + if( !before_core_hardfork_834 ) + usd_to_buy.amount = call_itr->get_max_debt_to_cover( match_price, + bitasset.current_feed.settlement_price, + bitasset.current_feed.maintenance_collateral_ratio ); - filled_limit_in_loop = true; + asset call_pays, call_receives, order_pays, order_receives; + if( usd_to_buy > usd_for_sale ) + { // fill order + order_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): + // * after hard fork core-625, the limit order will be always a maker if entered this function; + // * before hard fork core-625, + // * when the limit order is a taker, it could be paying something for nothing only when + // the call order is smaller and is too small + // * when the limit order is a maker, it won't be paying something for nothing + if( order_receives.amount == 0 ) // TODO this should not happen. remove the warning after confirmed + { + if( before_core_hardfork_184 ) + wlog( "Something for nothing issue (#184, variant D-1) occurred at block #${block}", ("block",head_block_num()) ); + else + wlog( "Something for nothing issue (#184, variant D-2) occurred at block #${block}", ("block",head_block_num()) ); + } + + if( before_core_hardfork_342 ) + call_receives = usd_for_sale; + else + // The remaining amount in the limit order would be too small, + // 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 ^ match_price; filled_limit = true; - filled_call = (usd_to_buy == usd_for_sale); } else { // fill call call_receives = usd_to_buy; - order_receives = usd_to_buy * match_price; // round down, in favor of call order - call_pays = order_receives; - order_pays = usd_to_buy; + if( before_core_hardfork_342 ) + { + order_receives = usd_to_buy * match_price; // round down, in favor of call order - filled_call = true; - if( filled_limit && maint_time <= HARDFORK_CORE_453_TIME ) + // Be here, the limit order would be paying something for nothing + if( order_receives.amount == 0 ) // TODO remove warning after hard fork core-342 + wlog( "Something for nothing issue (#184, variant D) occurred at block #${block}", ("block",head_block_num()) ); + } + else + order_receives = usd_to_buy ^ 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) + if( usd_to_buy == usd_for_sale ) + filled_limit = true; + else if( filled_limit && maint_time <= HARDFORK_CORE_453_TIME ) // TODO remove warning after hard fork core-453 wlog( "Multiple limit match problem (issue 453) occurred at block #${block}", ("block",head_block_num()) ); } - FC_ASSERT( filled_call || filled_limit ); - FC_ASSERT( filled_call || filled_limit_in_loop ); - - // Be here, the call order won't be paying something for nothing according to code above. - // After hard fork core-625, the limit order will be always a maker if entered this function, - // so it won't be paying something for nothing, since if it would, it would have been cancelled already. - // However, we need to check if it's culled after partially filled. - // Before hard fork core-625, - // when the limit order is a taker, it could be paying something for nothing here; - // when the limit order is a maker, it won't be paying something for nothing, - // however, if it's culled after partially filled, `limit_itr` may be invalidated so should not be dereferenced - if( order_receives.amount == 0 ) - { - if( maint_time > HARDFORK_CORE_184_TIME ) - { - if( filled_call ) // call would be completely filled // should always be true - { - order_receives.amount = 1; // round up to 1 Satoshi - call_pays = order_receives; - } - // else do nothing, since the limit order should have already been cancelled elsewhere - else // TODO remove warning after confirmed - wlog( "Something for nothing issue (#184, variant D-1) occurred at block #${block}", ("block",head_block_num()) ); - } - else // TODO remove warning after hard fork core-184 - wlog( "Something for nothing issue (#184, variant D) occurred at block #${block}", ("block",head_block_num()) ); - } + call_pays = order_receives; + order_pays = call_receives; auto old_call_itr = call_itr; - if( filled_call && maint_time <= HARDFORK_CORE_343_TIME ) + if( filled_call && before_core_hardfork_343 ) ++call_itr; // when for_new_limit_order is true, the call order is maker, otherwise the call order is taker fill_call_order(*old_call_itr, call_pays, call_receives, match_price, for_new_limit_order ); - if( maint_time > HARDFORK_CORE_343_TIME ) + if( !before_core_hardfork_343 ) call_itr = call_price_index.lower_bound( call_min ); 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_itr, order_pays, order_receives, true, match_price, !for_new_limit_order ); - if( really_filled || ( filled_limit && maint_time <= HARDFORK_CORE_453_TIME ) ) + if( really_filled || ( filled_limit && before_core_hardfork_453 ) ) limit_itr = next_limit_itr; } // while call_itr != call_end diff --git a/libraries/chain/db_update.cpp b/libraries/chain/db_update.cpp index 6f704b77..731a6a36 100644 --- a/libraries/chain/db_update.cpp +++ b/libraries/chain/db_update.cpp @@ -289,15 +289,17 @@ bool database::check_for_blackswan( const asset_object& mia, bool enable_black_s if( ~least_collateral >= highest ) { wdump( (*call_itr) ); - elog( "Black Swan detected: \n" + elog( "Black Swan detected on asset ${symbol} (${id}) at block ${b}: \n" " Least collateralized call: ${lc} ${~lc}\n" // " Highest Bid: ${hb} ${~hb}\n" " Settle Price: ${~sp} ${sp}\n" " Max: ${~h} ${h}\n", + ("id",mia.id)("symbol",mia.symbol)("b",head_block_num()) ("lc",least_collateral.to_real())("~lc",(~least_collateral).to_real()) // ("hb",limit_itr->sell_price.to_real())("~hb",(~limit_itr->sell_price).to_real()) ("sp",settle_price.to_real())("~sp",(~settle_price).to_real()) ("h",highest.to_real())("~h",(~highest).to_real()) ); + edump((enable_black_swan)); FC_ASSERT( enable_black_swan, "Black swan was detected during a margin update which is not allowed to trigger a blackswan" ); if( maint_time > HARDFORK_CORE_338_TIME && ~least_collateral <= settle_price ) // globol settle at feed price if possible @@ -311,25 +313,30 @@ bool database::check_for_blackswan( const asset_object& mia, bool enable_black_s void database::clear_expired_orders() { try { - //Cancel expired limit orders - auto& limit_index = get_index_type().indices().get(); - while( !limit_index.empty() && limit_index.begin()->expiration <= head_block_time() ) - { - const limit_order_object& order = *limit_index.begin(); - auto base_asset = order.sell_price.base.asset_id; - auto quote_asset = order.sell_price.quote.asset_id; - cancel_limit_order( order ); - // check call orders - if( head_block_time() > HARDFORK_CORE_604_TIME ) - { - // Comments below are copied from limit_order_cancel_evaluator::do_apply(...) - // Possible optimization: order can be called by cancelling a limit order - // if the canceled order was at the top of the book. - // Do I need to check calls in both assets? - check_call_orders( base_asset( *this ) ); - check_call_orders( quote_asset( *this ) ); - } - } + //Cancel expired limit orders + auto head_time = head_block_time(); + auto maint_time = get_dynamic_global_properties().next_maintenance_time; + bool before_core_hardfork_184 = ( maint_time <= HARDFORK_CORE_184_TIME ); // something-for-nothing + bool before_core_hardfork_342 = ( maint_time <= HARDFORK_CORE_342_TIME ); // better rounding + bool before_core_hardfork_606 = ( maint_time <= HARDFORK_CORE_606_TIME ); // feed always trigger call + auto& limit_index = get_index_type().indices().get(); + while( !limit_index.empty() && limit_index.begin()->expiration <= head_time ) + { + const limit_order_object& order = *limit_index.begin(); + auto base_asset = order.sell_price.base.asset_id; + auto quote_asset = order.sell_price.quote.asset_id; + cancel_limit_order( order ); + if( before_core_hardfork_606 ) + { + // check call orders + // Comments below are copied from limit_order_cancel_evaluator::do_apply(...) + // Possible optimization: order can be called by cancelling a limit order + // if the canceled order was at the top of the book. + // Do I need to check calls in both assets? + check_call_orders( base_asset( *this ) ); + check_call_orders( quote_asset( *this ) ); + } + } //Process expired force settlement orders auto& settlement_index = get_index_type().indices().get(); @@ -338,6 +345,7 @@ void database::clear_expired_orders() asset_id_type current_asset = settlement_index.begin()->settlement_asset_id(); asset max_settlement_volume; price settlement_fill_price; + price settlement_price; bool current_asset_finished = false; bool extra_dump = false; @@ -390,7 +398,7 @@ void database::clear_expired_orders() } // Has this order not reached its settlement date? - if( order.settlement_date > head_block_time() ) + if( order.settlement_date > head_time() ) { if( next_asset() ) { @@ -432,23 +440,23 @@ void database::clear_expired_orders() break; } - auto& pays = order.balance; - auto receives = (order.balance * mia.current_feed.settlement_price); - receives.amount = (fc::uint128_t(receives.amount.value) * - (GRAPHENE_100_PERCENT - mia.options.force_settlement_offset_percent) / GRAPHENE_100_PERCENT).to_uint64(); - assert(receives <= order.balance * mia.current_feed.settlement_price); - - price settlement_price = pays / receives; - - // Calculate fill_price with a bigger volume to reduce impacts of rounding if( settlement_fill_price.base.asset_id != pays.asset_id ) + settlement_fill_price = mia.current_feed.settlement_price + / ratio_type( GRAPHENE_100_PERCENT - mia.options.force_settlement_offset_percent, + GRAPHENE_100_PERCENT ); + + if( before_core_hardfork_342 ) { - asset tmp_pays = max_settlement_volume; - asset tmp_receives = tmp_pays * mia.current_feed.settlement_price; - tmp_receives.amount = (fc::uint128_t(tmp_receives.amount.value) * - (GRAPHENE_100_PERCENT - mia.options.force_settlement_offset_percent) / GRAPHENE_100_PERCENT).to_uint64(); - settlement_fill_price = tmp_pays / tmp_receives; + auto& pays = order.balance; + auto receives = (order.balance * mia.current_feed.settlement_price); + receives.amount = ( fc::uint128_t(receives.amount.value) * + (GRAPHENE_100_PERCENT - mia.options.force_settlement_offset_percent) / + GRAPHENE_100_PERCENT ).to_uint64(); + assert(receives <= order.balance * mia.current_feed.settlement_price); + settlement_price = pays / receives; } + else if( settlement_price.base.asset_id != current_asset ) // only calculate once per asset + settlement_price = settlement_fill_price; auto& call_index = get_index_type().indices().get(); asset settled = mia_object.amount(mia.force_settled_volume); @@ -469,16 +477,27 @@ void database::clear_expired_orders() } try { asset new_settled = match(*itr, order, settlement_price, max_settlement, settlement_fill_price); - if( maint_time > HARDFORK_CORE_184_TIME && new_settled.amount == 0 ) // unable to fill this settle order + if( !before_core_hardfork_184 && new_settled.amount == 0 ) // unable to fill this settle order { if( find_object( order_id ) ) // the settle order hasn't been cancelled current_asset_finished = true; break; } settled += new_settled; + // before hard fork core-342, if new_settled > 0, we'll have: + // * call order is completely filled (thus itr will change in next loop), or + // * settle order is completely filled (thus find_object(order_id) will be false so will break out), or + // * reached max_settlement_volume limit (thus new_settled == max_settlement so will break out). + // + // after hard fork core-342, if new_settled > 0, we'll have: + // * call order is completely filled (thus itr will change in next loop), or + // * settle order is completely filled (thus find_object(order_id) will be false so will break out), or + // * reached max_settlement_volume limit, but it's possible that new_settled < max_settlement, + // in this case, new_settled will be zero in next iteration of the loop, so no need to check here. } catch ( const black_swan_exception& e ) { - wlog( "black swan detected: ${e}", ("e", e.to_detail_string() ) ); + wlog( "Cancelling a settle_order since it may trigger a black swan: ${o}, ${e}", + ("o", order)("e", e.to_detail_string()) ); cancel_settle_order( order ); break; } diff --git a/libraries/chain/hardfork.d/CORE_834.hf b/libraries/chain/hardfork.d/CORE_834.hf new file mode 100644 index 00000000..864b1de3 --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_834.hf @@ -0,0 +1,4 @@ +// bitshares-core issue #834 "BSIP38: add target CR option to short positions" +#ifndef HARDFORK_CORE_834_TIME +#define HARDFORK_CORE_834_TIME (fc::time_point_sec( 1615334400 )) // Wednesday, 10 March 2021 00:00:00 UTC +#endif \ No newline at end of file diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index 14b3022b..dd85ec98 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -433,7 +433,8 @@ namespace graphene { namespace chain { */ ///@{ int match( const limit_order_object& taker, const limit_order_object& maker, const price& trade_price ); - int match( const limit_order_object& taker, const call_order_object& maker, const price& trade_price ); + 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 ); /// @return the amount of asset settled asset match(const call_order_object& call, const force_settlement_object& settle, diff --git a/libraries/chain/include/graphene/chain/market_object.hpp b/libraries/chain/include/graphene/chain/market_object.hpp index 45a88de7..b87a7af5 100644 --- a/libraries/chain/include/graphene/chain/market_object.hpp +++ b/libraries/chain/include/graphene/chain/market_object.hpp @@ -125,12 +125,16 @@ class call_order_object : public abstract_object share_type debt; ///< call_price.quote.asset_id, access via get_debt price call_price; ///< Collateral / Debt + optional target_collateral_ratio; ///< maximum CR to maintain when selling collateral on margin call + pair get_market()const { auto tmp = std::make_pair( call_price.base.asset_id, call_price.quote.asset_id ); if( tmp.first > tmp.second ) std::swap( tmp.first, tmp.second ); return tmp; } + /// Calculate maximum quantity of debt to cover to satisfy @ref target_collateral_ratio. + share_type get_max_debt_to_cover( price match_price, price feed_price, const uint16_t maintenance_collateral_ratio )const; }; /** @@ -215,7 +219,7 @@ FC_REFLECT_DERIVED( graphene::chain::limit_order_object, ) FC_REFLECT_DERIVED( graphene::chain::call_order_object, (graphene::db::object), - (borrower)(collateral)(debt)(call_price) ) + (borrower)(collateral)(debt)(call_price)(target_collateral_ratio) ) FC_REFLECT_DERIVED( graphene::chain::force_settlement_object, (graphene::db::object), diff --git a/libraries/chain/include/graphene/chain/protocol/market.hpp b/libraries/chain/include/graphene/chain/protocol/market.hpp index d4725e02..da0b0b98 100644 --- a/libraries/chain/include/graphene/chain/protocol/market.hpp +++ b/libraries/chain/include/graphene/chain/protocol/market.hpp @@ -24,6 +24,7 @@ #pragma once #include #include +#include namespace graphene { namespace chain { @@ -111,6 +112,15 @@ namespace graphene { namespace chain { */ struct call_order_update_operation : public base_operation { + /** + * Options to be used in @ref call_order_update_operation. + * + * @note this struct can be expanded by adding more options in the end. + */ + struct options_type + { + optional target_collateral_ratio; ///< maximum CR to maintain when selling collateral on margin call + }; /** this is slightly more expensive than limit orders, this pricing impacts prediction markets */ struct fee_parameters_type { uint64_t fee = 20 * GRAPHENE_BLOCKCHAIN_PRECISION; }; @@ -118,6 +128,7 @@ namespace graphene { namespace chain { account_id_type funding_account; ///< pays fee, collateral, and cover asset delta_collateral; ///< the amount of collateral to add to the margin position asset delta_debt; ///< the amount of the debt to be paid off, may be negative to issue new debt + typedef extension extensions_type; // note: this will be jsonified to {...} but no longer [...] extensions_type extensions; account_id_type fee_payer()const { return funding_account; } @@ -173,6 +184,10 @@ FC_REFLECT( graphene::chain::limit_order_cancel_operation,(fee)(fee_paying_accou FC_REFLECT( graphene::chain::call_order_update_operation, (fee)(funding_account)(delta_collateral)(delta_debt)(extensions) ) FC_REFLECT( graphene::chain::fill_order_operation, (fee)(order_id)(account_id)(pays)(receives)(fill_price)(is_maker) ) +FC_REFLECT( graphene::chain::call_order_update_operation::options_type, (target_collateral_ratio) ) + +FC_REFLECT_TYPENAME( graphene::chain::call_order_update_operation::extensions_type + GRAPHENE_EXTERNAL_SERIALIZATION( extern, graphene::chain::limit_order_create_operation::fee_parameters_type ) GRAPHENE_EXTERNAL_SERIALIZATION( extern, graphene::chain::limit_order_cancel_operation::fee_parameters_type ) GRAPHENE_EXTERNAL_SERIALIZATION( extern, graphene::chain::call_order_update_operation::fee_parameters_type ) diff --git a/libraries/chain/market_evaluator.cpp b/libraries/chain/market_evaluator.cpp index 6f944a8e..a6a21165 100644 --- a/libraries/chain/market_evaluator.cpp +++ b/libraries/chain/market_evaluator.cpp @@ -158,6 +158,11 @@ void_result call_order_update_evaluator::do_evaluate(const call_order_update_ope { try { database& d = db(); + // TODO: remove this check and the assertion after hf_834 + if( d.get_dynamic_global_properties().next_maintenance_time <= HARDFORK_CORE_834_TIME ) + FC_ASSERT( !o.extensions.value.target_collateral_ratio.valid(), + "Can not set target_collateral_ratio in call_order_update_operation before hardfork 834." ); + _paying_account = &o.funding_account(d); _debt_asset = &o.delta_debt.asset_id(d); FC_ASSERT( _debt_asset->is_market_issued(), "Unable to cover ${sym} as it is not a collateralized asset.", @@ -227,6 +232,8 @@ void_result call_order_update_evaluator::do_apply(const call_order_update_operat auto itr = call_idx.find( boost::make_tuple(o.funding_account, o.delta_debt.asset_id) ); const call_order_object* call_obj = nullptr; + optional new_target_cr = o.extensions.value.target_collateral_ratio; + if( itr == call_idx.end() ) { FC_ASSERT( o.delta_collateral.amount > 0 ); @@ -238,6 +245,7 @@ void_result call_order_update_evaluator::do_apply(const call_order_update_operat call.debt = o.delta_debt.amount; call.call_price = price::call_price(o.delta_debt, o.delta_collateral, _bitasset_data->current_feed.maintenance_collateral_ratio); + call.target_collateral_ratio = new_target_cr; }); } @@ -246,13 +254,15 @@ void_result call_order_update_evaluator::do_apply(const call_order_update_operat call_obj = &*itr; d.modify( *call_obj, [&]( call_order_object& call ){ + call.collateral += o.delta_collateral.amount; call.collateral += o.delta_collateral.amount; call.debt += o.delta_debt.amount; if( call.debt > 0 ) { call.call_price = price::call_price(call.get_debt(), call.get_collateral(), - _bitasset_data->current_feed.maintenance_collateral_ratio); + _bitasset_data->current_feed.maintenance_collateral_ratio); } + call.target_collateral_ratio = new_target_cr; }); } diff --git a/libraries/chain/market_object.cpp b/libraries/chain/market_object.cpp new file mode 100644 index 00000000..ee203eb7 --- /dev/null +++ b/libraries/chain/market_object.cpp @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2018 Abit More, and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include + +#include + +using namespace graphene::chain; + +/* +target_CR = max( target_CR, MCR ) +target_CR = new_collateral / ( new_debt / feed_price ) + = ( collateral - max_amount_to_sell ) * feed_price + / ( debt - amount_to_get ) + = ( collateral - max_amount_to_sell ) * feed_price + / ( debt - round_down(max_amount_to_sell * match_price ) ) + = ( collateral - max_amount_to_sell ) * feed_price + / ( debt - (max_amount_to_sell * match_price - x) ) +Note: x is the fraction, 0 <= x < 1 +=> +max_amount_to_sell = ( (debt + x) * target_CR - collateral * feed_price ) + / (target_CR * match_price - feed_price) + = ( (debt + x) * tCR / DENOM - collateral * fp_debt_amt / fp_coll_amt ) + / ( (tCR / DENOM) * (mp_debt_amt / mp_coll_amt) - fp_debt_amt / fp_coll_amt ) + = ( (debt + x) * tCR * fp_coll_amt * mp_coll_amt - collateral * fp_debt_amt * DENOM * mp_coll_amt) + / ( tCR * mp_debt_amt * fp_coll_amt - fp_debt_amt * DENOM * mp_coll_amt ) +max_debt_to_cover = max_amount_to_sell * match_price + = max_amount_to_sell * mp_debt_amt / mp_coll_amt + = ( (debt + x) * tCR * fp_coll_amt * mp_debt_amt - collateral * fp_debt_amt * DENOM * mp_debt_amt) + / (tCR * mp_debt_amt * fp_coll_amt - fp_debt_amt * DENOM * mp_coll_amt) +*/ +share_type call_order_object::get_max_debt_to_cover( price match_price, + price feed_price, + const uint16_t maintenance_collateral_ratio )const +{ try { + // be defensive here, make sure feed_price is in collateral / debt format + if( feed_price.base.asset_id != call_price.base.asset_id ) + feed_price = ~feed_price; + + FC_ASSERT( feed_price.base.asset_id == call_price.base.asset_id + && feed_price.quote.asset_id == call_price.quote.asset_id ); + + if( call_price > feed_price ) // feed protected. be defensive here, although this should be guaranteed by caller + return 0; + + if( !target_collateral_ratio.valid() ) // target cr is not set + return debt; + + uint16_t tcr = std::max( *target_collateral_ratio, maintenance_collateral_ratio ); // use mcr if target cr is too small + + // be defensive here, make sure match_price is in collateral / debt format + if( match_price.base.asset_id != call_price.base.asset_id ) + match_price = ~match_price; + + FC_ASSERT( match_price.base.asset_id == call_price.base.asset_id + && match_price.quote.asset_id == call_price.quote.asset_id ); + + typedef boost::multiprecision::int256_t i256; + i256 mp_debt_amt = match_price.quote.amount.value; + i256 mp_coll_amt = match_price.base.amount.value; + i256 fp_debt_amt = feed_price.quote.amount.value; + i256 fp_coll_amt = feed_price.base.amount.value; + + // firstly we calculate without the fraction (x), the result could be a bit too small + i256 numerator = fp_coll_amt * mp_debt_amt * debt.value * tcr + - fp_debt_amt * mp_debt_amt * collateral.value * GRAPHENE_COLLATERAL_RATIO_DENOM; + if( numerator < 0 ) // feed protected, actually should not be true here, just check to be safe + return 0; + + i256 denominator = fp_coll_amt * mp_debt_amt * tcr - fp_debt_amt * mp_coll_amt * GRAPHENE_COLLATERAL_RATIO_DENOM; + if( denominator <= 0 ) // black swan + return debt; + + // note: if add 1 here, will result in 1.5x imperfection rate; + // however, due to rounding, the result could still be a bit too big, thus imperfect. + i256 to_cover_i256 = ( numerator / denominator ); + if( to_cover_i256 >= debt.value ) // avoid possible overflow + return debt; + share_type to_cover_amt = static_cast< int64_t >( to_cover_i256 ); + + // stabilize + // note: rounding up-down results in 3x imperfection rate in comparison to down-down-up + asset to_pay = asset( to_cover_amt, debt_type() ) * match_price; + asset to_cover = to_pay * match_price; + to_pay = to_cover.multiply_and_round_up( match_price ); + + if( to_cover.amount >= debt || to_pay.amount >= collateral ) // to be safe + return debt; + FC_ASSERT( to_pay.amount < collateral && to_cover.amount < debt ); + + // check collateral ratio after filled, if it's OK, we return + price new_call_price = price::call_price( get_debt() - to_cover, get_collateral() - to_pay, tcr ); + if( new_call_price > feed_price ) + return to_cover.amount; + + // be here, to_cover is too small due to rounding. deal with the fraction + numerator += fp_coll_amt * mp_debt_amt * tcr; // plus the fraction + to_cover_i256 = ( numerator / denominator ) + 1; + if( to_cover_i256 >= debt.value ) // avoid possible overflow + to_cover_i256 = debt.value; + to_cover_amt = static_cast< int64_t >( to_cover_i256 ); + + asset max_to_pay = ( ( to_cover_amt == debt.value ) ? get_collateral() + : asset( to_cover_amt, debt_type() ).multiply_and_round_up( match_price ) ); + if( max_to_pay.amount > collateral ) + max_to_pay.amount = collateral; + + asset max_to_cover = ( ( max_to_pay.amount == collateral ) ? get_debt() : ( max_to_pay * match_price ) ); + if( max_to_cover.amount >= debt ) // to be safe + { + max_to_pay.amount = collateral; + max_to_cover.amount = debt; + } + + if( max_to_pay <= to_pay || max_to_cover <= to_cover ) // strange data. should skip binary search and go on, but doesn't help much + return debt; + FC_ASSERT( max_to_pay > to_pay && max_to_cover > to_cover ); + + asset min_to_pay = to_pay; + asset min_to_cover = to_cover; + + // try with binary search to find a good value + // note: actually binary search can not always provide perfect result here, + // due to rounding, collateral ratio is not always increasing while to_pay or to_cover is increasing + bool max_is_ok = false; + while( true ) + { + // get the mean + if( match_price.base.amount < match_price.quote.amount ) // step of collateral is smaller + { + to_pay.amount = ( min_to_pay.amount + max_to_pay.amount + 1 ) / 2; // should not overflow. round up here + if( to_pay.amount == max_to_pay.amount ) + to_cover.amount = max_to_cover.amount; + else + { + to_cover = to_pay * match_price; + if( to_cover.amount >= max_to_cover.amount ) // can be true when max_is_ok is false + { + to_pay.amount = max_to_pay.amount; + to_cover.amount = max_to_cover.amount; + } + else + { + to_pay = to_cover.multiply_and_round_up( match_price ); // stabilization, no change or become smaller + FC_ASSERT( to_pay.amount < max_to_pay.amount ); + } + } + } + else // step of debt is smaller or equal + { + to_cover.amount = ( min_to_cover.amount + max_to_cover.amount ) / 2; // should not overflow. round down here + if( to_cover.amount == max_to_cover.amount ) + to_pay.amount = max_to_pay.amount; + else + { + to_pay = to_cover.multiply_and_round_up( match_price ); + if( to_pay.amount >= max_to_pay.amount ) // can be true when max_is_ok is false + { + to_pay.amount = max_to_pay.amount; + to_cover.amount = max_to_cover.amount; + } + else + { + to_cover = to_pay * match_price; // stabilization, to_cover should have increased + if( to_cover.amount >= max_to_cover.amount ) // to be safe + { + to_pay.amount = max_to_pay.amount; + to_cover.amount = max_to_cover.amount; + } + } + } + } + + // check again to see if we've moved away from the minimums, if not, use the maximums directly + if( to_pay.amount <= min_to_pay.amount || to_cover.amount <= min_to_cover.amount + || to_pay.amount > max_to_pay.amount || to_cover.amount > max_to_cover.amount ) + { + to_pay.amount = max_to_pay.amount; + to_cover.amount = max_to_cover.amount; + } + + // check the mean + if( to_pay.amount == max_to_pay.amount && ( max_is_ok || to_pay.amount == collateral ) ) + return to_cover.amount; + FC_ASSERT( to_pay.amount < collateral && to_cover.amount < debt ); + + new_call_price = price::call_price( get_debt() - to_cover, get_collateral() - to_pay, tcr ); + if( new_call_price > feed_price ) // good + { + if( to_pay.amount == max_to_pay.amount ) + return to_cover.amount; + max_to_pay.amount = to_pay.amount; + max_to_cover.amount = to_cover.amount; + max_is_ok = true; + } + else // not good + { + if( to_pay.amount == max_to_pay.amount ) + break; + min_to_pay.amount = to_pay.amount; + min_to_cover.amount = to_cover.amount; + } + } + + // be here, max_to_cover is too small due to rounding. search forward + for( uint64_t d1 = 0, d2 = 1, d3 = 1; ; d1 = d2, d2 = d3, d3 = d1 + d2 ) // 1,1,2,3,5,8,... + { + if( match_price.base.amount > match_price.quote.amount ) // step of debt is smaller + { + to_pay.amount += d2; + if( to_pay.amount >= collateral ) + return debt; + to_cover = to_pay * match_price; + if( to_cover.amount >= debt ) + return debt; + to_pay = to_cover.multiply_and_round_up( match_price ); // stabilization + if( to_pay.amount >= collateral ) + return debt; + } + else // step of collateral is smaller or equal + { + to_cover.amount += d2; + if( to_cover.amount >= debt ) + return debt; + to_pay = to_cover.multiply_and_round_up( match_price ); + if( to_pay.amount >= collateral ) + return debt; + to_cover = to_pay * match_price; // stabilization + if( to_cover.amount >= debt ) + return debt; + } + + // check + FC_ASSERT( to_pay.amount < collateral && to_cover.amount < debt ); + + new_call_price = price::call_price( get_debt() - to_cover, get_collateral() - to_pay, tcr ); + if( new_call_price > feed_price ) // good + return to_cover.amount; + } + +} FC_CAPTURE_AND_RETHROW( (*this)(feed_price)(match_price)(maintenance_collateral_ratio) ) } \ No newline at end of file