fixes bts mid 2018

This commit is contained in:
sierra19XX 2021-03-12 12:25:46 +11:00
parent 4d8f3725b0
commit 463c812a33
9 changed files with 588 additions and 173 deletions

View file

@ -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); FC_ASSERT(asset_to_settle->dynamic_data(d).current_supply > 0);
const asset_bitasset_data_object* bitasset_data = &asset_to_settle->bitasset_data(d); 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 /// 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" ); FC_ASSERT( !bitasset_data->has_settlement(),"This asset has settlement, cannot global settle twice" );
} }

View file

@ -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<call_order_index>(); const call_order_index& call_index = get_index_type<call_order_index>();
const auto& call_price_index = call_index.indices().get<by_price>(); const auto& call_price_index = call_index.indices().get<by_price>();
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 // 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_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 ) ); auto call_end = call_price_index.upper_bound( price::max( bitasset.options.short_backing_asset, mia.id ) );
asset pays;
while( call_itr != call_end ) 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() ) if( pays > call_itr->get_collateral() )
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; collateral_gathered += pays;
const auto& order = *call_itr; 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 // check if there are margin calls
const auto& call_price_idx = get_index_type<call_order_index>().indices().get<by_price>(); const auto& call_price_idx = get_index_type<call_order_index>().indices().get<by_price>();
auto call_min = price::min( recv_asset_id, sell_asset_id ); auto call_min = price::min( recv_asset_id, sell_asset_id );
auto call_itr = call_price_idx.lower_bound( call_min ); while( !finished )
// 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 )
{ {
auto old_call_itr = call_itr; // assume hard fork core-343 and core-625 will take place at same time, always check call order with least call_price
++call_itr; // would be safe, since we'll end the loop if a call order is partially matched auto call_itr = call_price_idx.lower_bound( call_min );
// match returns 2 when only the old order was fully filled. In this case, we keep matching; otherwise, we stop. 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. // 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 ) 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 ); FC_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 ); FC_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.for_sale > 0 && core.for_sale > 0 );
auto usd_for_sale = usd.amount_for_sale(); auto usd_for_sale = usd.amount_for_sale();
auto core_for_sale = core.amount_for_sale(); auto core_for_sale = core.amount_for_sale();
asset usd_pays, usd_receives, core_pays, core_receives; 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 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 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 //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 //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. //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 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; core_pays = usd_receives;
usd_pays = core_receives; usd_pays = core_receives;
assert( usd_pays == usd.amount_for_sale() || if( before_core_hardfork_342 )
core_pays == core.amount_for_sale() ); FC_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;
int result = 0; 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 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; 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.sell_asset_id() == ask.debt_type() );
FC_ASSERT( bid.receive_asset_id() == ask.collateral_type() ); FC_ASSERT( bid.receive_asset_id() == ask.collateral_type() );
FC_ASSERT( bid.for_sale > 0 && ask.debt > 0 && ask.collateral > 0 ); FC_ASSERT( bid.for_sale > 0 && ask.debt > 0 && ask.collateral > 0 );
bool filled_limit = false; auto maint_time = get_dynamic_global_properties().next_maintenance_time;
bool filled_call = false; // 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_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; 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 { // fill limit order
call_receives = usd_for_sale;
order_receives = usd_for_sale * match_price; // round down here, in favor of call order 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; // Be here, it's possible that taker is paying something for nothing due to partially filled in last loop.
filled_call = ( usd_to_buy == usd_for_sale ); // 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 else
{ // fill call order { // fill call order
call_receives = usd_to_buy; call_receives = usd_to_buy;
order_receives = usd_to_buy * match_price; // round down here, in favor of call order if( before_core_hardfork_342 ) // TODO remove this "if" when we're sure it's always false (keep the code in else)
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 )
{ {
order_receives.amount = 1; order_receives = usd_to_buy * match_price; // round down here, in favor of call order
call_pays = order_receives; // 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 else // has hardfork core-342
// It's possible that taker is paying something for nothing due to partially filled in last loop. order_receives = usd_to_buy ^ match_price; // round up here, in favor of limit order
// In this case, we see it as filled and cancel it later
return 1;
} }
call_pays = order_receives;
order_pays = call_receives;
int result = 0; 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 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; 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.get_debt().asset_id == settle.balance.asset_id );
FC_ASSERT(call.debt > 0 && call.collateral > 0 && settle.balance.amount > 0); 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 settle_for_sale = std::min(settle.balance, max_settlement);
auto call_debt = call.get_debt(); auto call_debt = call.get_debt();
asset call_receives = std::min(settle_for_sale, call_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. // 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( 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 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()) ); wlog( "Something for nothing issue (#184, variant C-2) handled at block #${block}", ("block",head_block_num()) );
cancel_settle_order( settle ); cancel_settle_order( settle );
} }
else // neither order will be completely filled, perhaps due to max_settlement too small // else do nothing: 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()) );
return asset( 0, settle.balance.asset_id ); return asset( 0, settle.balance.asset_id );
} }
} }
else else
wlog( "Something for nothing issue (#184, variant C) occurred at block #${block}", ("block",head_block_num()) ); 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_pays = call_receives;
asset settle_receives = call_pays; 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 * can trigger a black swan. So now we must cancel the forced settlement
* object. * 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_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 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; return call_receives;
} FC_CAPTURE_AND_RETHROW( (call)(settle)(match_price)(max_settlement) ) } } 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); 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); const account_statistics_object& borrower_statistics = borrower.statistics(*this);
if( collateral_freed ) if( collateral_freed.valid() )
adjust_balance(borrower.get_id(), *collateral_freed); adjust_balance(borrower.get_id(), *collateral_freed);
modify( borrower_statistics, [&]( account_statistics_object& b ){ 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; b.total_core_in_orders -= collateral_freed->amount;
if( pays.asset_id == asset_id_type() ) if( pays.asset_id == asset_id_type() )
b.total_core_in_orders -= pays.amount; 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, push_applied_operation( fill_order_operation( order.id, order.borrower, pays, receives,
asset(0, pays.asset_id), fill_price, is_maker ) ); asset(0, pays.asset_id), fill_price, is_maker ) );
if( collateral_freed ) if( collateral_freed.valid() )
remove( order ); remove( order );
return collateral_freed.valid(); 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, 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 ) const asset_bitasset_data_object* bitasset_ptr )
{ try { { 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; if( !mia.is_market_issued() ) return false;
const asset_bitasset_data_object& bitasset = ( bitasset_ptr ? *bitasset_ptr : mia.bitasset_data(*this) ); 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_itr = call_price_index.lower_bound( call_min );
auto call_end = call_price_index.upper_bound( call_max ); 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_time = head_block_time();
auto head_num = head_block_num(); auto head_num = head_block_num();
bool after_hardfork_436 = ( head_time > HARDFORK_436_TIME ); bool after_hardfork_436 = ( head_time > HARDFORK_436_TIME );
auto head_time = head_block_time(); bool before_core_hardfork_184 = ( maint_time <= HARDFORK_CORE_184_TIME ); // something-for-nothing
auto maint_time = get_dynamic_global_properties().next_maintenance_time; 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 ) while( !check_for_blackswan( mia, enable_black_swan, &bitasset ) && call_itr != call_end )
{ {
bool filled_limit_in_loop = false;
bool filled_call = false; bool filled_call = false;
price match_price; price match_price;
asset usd_for_sale; asset usd_for_sale;
@ -807,12 +902,11 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa
match_price.validate(); match_price.validate();
// Feed protected (don't call if CR>MCR) https://github.com/cryptonomex/graphene/issues/436 // Feed protected (don't call if CR>MCR) https://github.com/cryptonomex/graphene/issues/436
if( ( head_time > HARDFORK_436_TIME ) if( after_hardfork_436 && ( bitasset.current_feed.settlement_price > ~call_itr->call_price ) )
&& ( bitasset.current_feed.settlement_price > ~call_itr->call_price ) )
return margin_called; return margin_called;
// Old rule: margin calls can only buy high https://github.com/bitshares/bitshares-core/issues/606 // 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; return margin_called;
margin_called = true; 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() ) if( usd_to_buy * match_price > call_itr->get_collateral() )
{ {
elog( "black swan detected on asset ${symbol} (${id}) at block ${b}", 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)); edump((enable_black_swan));
FC_ASSERT( enable_black_swan ); FC_ASSERT( enable_black_swan );
globally_settle_asset(mia, bitasset.current_feed.settlement_price ); globally_settle_asset(mia, bitasset.current_feed.settlement_price );
return true; return true;
} }
asset call_pays, call_receives, order_pays, order_receives; if( !before_core_hardfork_834 )
if( usd_to_buy >= usd_for_sale ) usd_to_buy.amount = call_itr->get_max_debt_to_cover( match_price,
{ // fill order bitasset.current_feed.settlement_price,
call_receives = usd_for_sale; bitasset.current_feed.maintenance_collateral_ratio );
order_receives = usd_for_sale * match_price; // round down, in favor of call order
call_pays = order_receives;
order_pays = usd_for_sale;
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_limit = true;
filled_call = (usd_to_buy == usd_for_sale);
} else { // fill call } else { // fill call
call_receives = usd_to_buy; call_receives = usd_to_buy;
order_receives = usd_to_buy * match_price; // round down, in favor of call order if( before_core_hardfork_342 )
call_pays = order_receives; {
order_pays = usd_to_buy; order_receives = usd_to_buy * match_price; // round down, in favor of call order
filled_call = true; // Be here, the limit order would be paying something for nothing
if( filled_limit && maint_time <= HARDFORK_CORE_453_TIME ) 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()) ); wlog( "Multiple limit match problem (issue 453) occurred at block #${block}", ("block",head_block_num()) );
} }
FC_ASSERT( filled_call || filled_limit ); call_pays = order_receives;
FC_ASSERT( filled_call || filled_limit_in_loop ); order_pays = call_receives;
// 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()) );
}
auto old_call_itr = call_itr; auto old_call_itr = call_itr;
if( filled_call && maint_time <= HARDFORK_CORE_343_TIME ) if( filled_call && before_core_hardfork_343 )
++call_itr; ++call_itr;
// when for_new_limit_order is true, the call order is maker, otherwise the call order is taker // 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 ); 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 ); call_itr = call_price_index.lower_bound( call_min );
auto next_limit_itr = std::next( limit_itr ); 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 // 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 ); 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; limit_itr = next_limit_itr;
} // while call_itr != call_end } // while call_itr != call_end

View file

@ -289,15 +289,17 @@ bool database::check_for_blackswan( const asset_object& mia, bool enable_black_s
if( ~least_collateral >= highest ) if( ~least_collateral >= highest )
{ {
wdump( (*call_itr) ); 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" " Least collateralized call: ${lc} ${~lc}\n"
// " Highest Bid: ${hb} ${~hb}\n" // " Highest Bid: ${hb} ${~hb}\n"
" Settle Price: ${~sp} ${sp}\n" " Settle Price: ${~sp} ${sp}\n"
" Max: ${~h} ${h}\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()) ("lc",least_collateral.to_real())("~lc",(~least_collateral).to_real())
// ("hb",limit_itr->sell_price.to_real())("~hb",(~limit_itr->sell_price).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()) ("sp",settle_price.to_real())("~sp",(~settle_price).to_real())
("h",highest.to_real())("~h",(~highest).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" ); 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 ) if( maint_time > HARDFORK_CORE_338_TIME && ~least_collateral <= settle_price )
// globol settle at feed price if possible // 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() void database::clear_expired_orders()
{ try { { try {
//Cancel expired limit orders //Cancel expired limit orders
auto& limit_index = get_index_type<limit_order_index>().indices().get<by_expiration>(); auto head_time = head_block_time();
while( !limit_index.empty() && limit_index.begin()->expiration <= 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
const limit_order_object& order = *limit_index.begin(); bool before_core_hardfork_342 = ( maint_time <= HARDFORK_CORE_342_TIME ); // better rounding
auto base_asset = order.sell_price.base.asset_id; bool before_core_hardfork_606 = ( maint_time <= HARDFORK_CORE_606_TIME ); // feed always trigger call
auto quote_asset = order.sell_price.quote.asset_id; auto& limit_index = get_index_type<limit_order_index>().indices().get<by_expiration>();
cancel_limit_order( order ); while( !limit_index.empty() && limit_index.begin()->expiration <= head_time )
// check call orders {
if( head_block_time() > HARDFORK_CORE_604_TIME ) const limit_order_object& order = *limit_index.begin();
{ auto base_asset = order.sell_price.base.asset_id;
// Comments below are copied from limit_order_cancel_evaluator::do_apply(...) auto quote_asset = order.sell_price.quote.asset_id;
// Possible optimization: order can be called by cancelling a limit order cancel_limit_order( order );
// if the canceled order was at the top of the book. if( before_core_hardfork_606 )
// Do I need to check calls in both assets? {
check_call_orders( base_asset( *this ) ); // check call orders
check_call_orders( quote_asset( *this ) ); // 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 //Process expired force settlement orders
auto& settlement_index = get_index_type<force_settlement_index>().indices().get<by_expiration>(); auto& settlement_index = get_index_type<force_settlement_index>().indices().get<by_expiration>();
@ -338,6 +345,7 @@ void database::clear_expired_orders()
asset_id_type current_asset = settlement_index.begin()->settlement_asset_id(); asset_id_type current_asset = settlement_index.begin()->settlement_asset_id();
asset max_settlement_volume; asset max_settlement_volume;
price settlement_fill_price; price settlement_fill_price;
price settlement_price;
bool current_asset_finished = false; bool current_asset_finished = false;
bool extra_dump = false; bool extra_dump = false;
@ -390,7 +398,7 @@ void database::clear_expired_orders()
} }
// Has this order not reached its settlement date? // Has this order not reached its settlement date?
if( order.settlement_date > head_block_time() ) if( order.settlement_date > head_time() )
{ {
if( next_asset() ) if( next_asset() )
{ {
@ -432,23 +440,23 @@ void database::clear_expired_orders()
break; 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 ) 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; auto& pays = order.balance;
asset tmp_receives = tmp_pays * mia.current_feed.settlement_price; auto receives = (order.balance * mia.current_feed.settlement_price);
tmp_receives.amount = (fc::uint128_t(tmp_receives.amount.value) * receives.amount = ( fc::uint128_t(receives.amount.value) *
(GRAPHENE_100_PERCENT - mia.options.force_settlement_offset_percent) / GRAPHENE_100_PERCENT).to_uint64(); (GRAPHENE_100_PERCENT - mia.options.force_settlement_offset_percent) /
settlement_fill_price = tmp_pays / tmp_receives; 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<call_order_index>().indices().get<by_collateral>(); auto& call_index = get_index_type<call_order_index>().indices().get<by_collateral>();
asset settled = mia_object.amount(mia.force_settled_volume); asset settled = mia_object.amount(mia.force_settled_volume);
@ -469,16 +477,27 @@ void database::clear_expired_orders()
} }
try { try {
asset new_settled = match(*itr, order, settlement_price, max_settlement, settlement_fill_price); 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 if( find_object( order_id ) ) // the settle order hasn't been cancelled
current_asset_finished = true; current_asset_finished = true;
break; break;
} }
settled += new_settled; 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 ) { 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 ); cancel_settle_order( order );
break; break;
} }

View file

@ -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

View file

@ -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 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 /// @return the amount of asset settled
asset match(const call_order_object& call, asset match(const call_order_object& call,
const force_settlement_object& settle, const force_settlement_object& settle,

View file

@ -125,12 +125,16 @@ class call_order_object : public abstract_object<call_order_object>
share_type debt; ///< call_price.quote.asset_id, access via get_debt share_type debt; ///< call_price.quote.asset_id, access via get_debt
price call_price; ///< Collateral / Debt price call_price; ///< Collateral / Debt
optional<uint16_t> target_collateral_ratio; ///< maximum CR to maintain when selling collateral on margin call
pair<asset_id_type,asset_id_type> get_market()const pair<asset_id_type,asset_id_type> get_market()const
{ {
auto tmp = std::make_pair( call_price.base.asset_id, call_price.quote.asset_id ); 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 ); if( tmp.first > tmp.second ) std::swap( tmp.first, tmp.second );
return tmp; 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), 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, FC_REFLECT_DERIVED( graphene::chain::force_settlement_object,
(graphene::db::object), (graphene::db::object),

View file

@ -24,6 +24,7 @@
#pragma once #pragma once
#include <graphene/chain/protocol/base.hpp> #include <graphene/chain/protocol/base.hpp>
#include <graphene/chain/protocol/asset.hpp> #include <graphene/chain/protocol/asset.hpp>
#include <graphene/chain/protocol/ext.hpp>
namespace graphene { namespace chain { namespace graphene { namespace chain {
@ -111,6 +112,15 @@ namespace graphene { namespace chain {
*/ */
struct call_order_update_operation : public base_operation 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<uint16_t> 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 */ /** this is slightly more expensive than limit orders, this pricing impacts prediction markets */
struct fee_parameters_type { uint64_t fee = 20 * GRAPHENE_BLOCKCHAIN_PRECISION; }; 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 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_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 asset delta_debt; ///< the amount of the debt to be paid off, may be negative to issue new debt
typedef extension<options_type> extensions_type; // note: this will be jsonified to {...} but no longer [...]
extensions_type extensions; extensions_type extensions;
account_id_type fee_payer()const { return funding_account; } 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::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::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_create_operation::fee_parameters_type )
GRAPHENE_EXTERNAL_SERIALIZATION( extern, graphene::chain::limit_order_cancel_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 ) GRAPHENE_EXTERNAL_SERIALIZATION( extern, graphene::chain::call_order_update_operation::fee_parameters_type )

View file

@ -158,6 +158,11 @@ void_result call_order_update_evaluator::do_evaluate(const call_order_update_ope
{ try { { try {
database& d = db(); 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); _paying_account = &o.funding_account(d);
_debt_asset = &o.delta_debt.asset_id(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.", 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) ); auto itr = call_idx.find( boost::make_tuple(o.funding_account, o.delta_debt.asset_id) );
const call_order_object* call_obj = nullptr; const call_order_object* call_obj = nullptr;
optional<uint16_t> new_target_cr = o.extensions.value.target_collateral_ratio;
if( itr == call_idx.end() ) if( itr == call_idx.end() )
{ {
FC_ASSERT( o.delta_collateral.amount > 0 ); 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.debt = o.delta_debt.amount;
call.call_price = price::call_price(o.delta_debt, o.delta_collateral, call.call_price = price::call_price(o.delta_debt, o.delta_collateral,
_bitasset_data->current_feed.maintenance_collateral_ratio); _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; call_obj = &*itr;
d.modify( *call_obj, [&]( call_order_object& call ){ d.modify( *call_obj, [&]( call_order_object& call ){
call.collateral += o.delta_collateral.amount;
call.collateral += o.delta_collateral.amount; call.collateral += o.delta_collateral.amount;
call.debt += o.delta_debt.amount; call.debt += o.delta_debt.amount;
if( call.debt > 0 ) if( call.debt > 0 )
{ {
call.call_price = price::call_price(call.get_debt(), call.get_collateral(), 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;
}); });
} }

View file

@ -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 <graphene/chain/market_object.hpp>
#include <boost/multiprecision/cpp_int.hpp>
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) ) }