From 26288549e608994200d3e9b8e4e5834458b50660 Mon Sep 17 00:00:00 2001 From: Daniel Larimer Date: Wed, 30 Sep 2015 18:30:44 -0400 Subject: [PATCH] Fix blackswan on force settle, Fix #346 and #347 - fix detection and handling of black swan events - fix total core in orders calculation from genesis --- libraries/chain/db_debug.cpp | 12 ++- libraries/chain/db_init.cpp | 6 ++ libraries/chain/db_market.cpp | 77 +++++++++++------ libraries/chain/db_update.cpp | 84 ++++++++++++++++++- .../chain/include/graphene/chain/database.hpp | 1 + .../include/graphene/chain/exceptions.hpp | 3 +- libraries/chain/market_evaluator.cpp | 4 + 7 files changed, 158 insertions(+), 29 deletions(-) diff --git a/libraries/chain/db_debug.cpp b/libraries/chain/db_debug.cpp index a792b741a7..0c2641563b 100644 --- a/libraries/chain/db_debug.cpp +++ b/libraries/chain/db_debug.cpp @@ -44,12 +44,12 @@ void database::debug_dump() for( const account_balance_object& a : balance_index ) { - idump(("balance")(a)); + // idump(("balance")(a)); total_balances[a.asset_type] += a.balance; } for( const account_statistics_object& s : statistics_index ) { - idump(("statistics")(s)); + // idump(("statistics")(s)); reported_core_in_orders += s.total_core_in_orders; } for( const limit_order_object& o : db.get_index_type().indices() ) @@ -71,17 +71,23 @@ void database::debug_dump() { total_balances[asset_obj.id] += asset_obj.dynamic_asset_data_id(db).accumulated_fees; total_balances[asset_id_type()] += asset_obj.dynamic_asset_data_id(db).fee_pool; +// edump((total_balances[asset_obj.id])(asset_obj.dynamic_asset_data_id(db).current_supply ) ); } + if( total_balances[asset_id_type()].value != core_asset_data.current_supply.value ) { edump( (total_balances[asset_id_type()].value)(core_asset_data.current_supply.value )); } + edump((core_in_orders)(reported_core_in_orders)); + + /* const auto& vbidx = db.get_index_type>(); for( const auto& s : vbidx ) { - idump(("vesting_balance")(s)); +// idump(("vesting_balance")(s)); } + */ } } } diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index ffbca39e47..1bd467f8cf 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -460,6 +460,10 @@ void database::init_genesis(const genesis_state_type& genesis_state) cop.active = cop.owner; account_id_type owner_account_id = apply_operation(genesis_eval_state, cop).get(); + modify( owner_account_id(*this).statistics(*this), [&]( account_statistics_object& o ) { + o.total_core_in_orders = collateral_rec.collateral; + }); + create([&](call_order_object& c) { c.borrower = owner_account_id; c.collateral = collateral_rec.collateral; @@ -611,6 +615,8 @@ void database::init_genesis(const genesis_state_type& genesis_state) wso.current_shuffled_witnesses.push_back( wid ); }); + debug_dump(); + _undo_db.enable(); } FC_CAPTURE_AND_RETHROW() } diff --git a/libraries/chain/db_market.cpp b/libraries/chain/db_market.cpp index 54fa666ace..f6b8739309 100644 --- a/libraries/chain/db_market.cpp +++ b/libraries/chain/db_market.cpp @@ -107,7 +107,9 @@ void database::cancel_order( const limit_order_object& order, bool create_virtua modify( order.seller(*this).statistics(*this),[&]( account_statistics_object& obj ){ if( refunded.asset_id == asset_id_type() ) + { obj.total_core_in_orders -= refunded.amount; + } }); adjust_balance(order.seller, refunded); @@ -220,19 +222,30 @@ int database::match( const limit_order_object& bid, const limit_order_object& as } -asset database::match( const call_order_object& call, const force_settlement_object& settle, const price& match_price, - asset max_settlement ) -{ - assert(call.get_debt().asset_id == settle.balance.asset_id ); - assert(call.debt > 0 && call.collateral > 0 && settle.balance.amount > 0); +asset database::match( const call_order_object& call, + const force_settlement_object& settle, + const price& match_price, + asset max_settlement ) +{ try { + FC_ASSERT(call.get_debt().asset_id == settle.balance.asset_id ); + FC_ASSERT(call.debt > 0 && call.collateral > 0 && settle.balance.amount > 0); 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), - call_pays = call_receives * match_price, - settle_pays = call_receives, - settle_receives = call_pays; + asset call_receives = std::min(settle_for_sale, call_debt); + asset call_pays = call_receives * match_price; + asset settle_pays = call_receives; + asset settle_receives = call_pays; + + /** + * If the least collateralized call position lacks sufficient + * collateral to cover at the match price then this indicates a black + * swan event according to the price feed, but only the market + * can trigger a black swan. So now we must cancel the forced settlement + * object. + */ + GRAPHENE_ASSERT( call_pays < call.get_collateral(), black_swan_exception, "" ); assert( settle_pays == settle_for_sale || call_receives == call.get_debt() ); @@ -240,12 +253,12 @@ asset database::match( const call_order_object& call, const force_settlement_obj fill_order(settle, settle_pays, settle_receives); return call_receives; -} +} FC_CAPTURE_AND_RETHROW( (call)(settle)(match_price)(max_settlement) ) } bool database::fill_order( const limit_order_object& order, const asset& pays, const asset& receives ) -{ - assert( order.amount_for_sale().asset_id == pays.asset_id ); - assert( pays.asset_id != receives.asset_id ); +{ try { + FC_ASSERT( order.amount_for_sale().asset_id == pays.asset_id ); + FC_ASSERT( pays.asset_id != receives.asset_id ); const account_object& seller = order.seller(*this); const asset_object& recv_asset = receives.asset_id(*this); @@ -279,15 +292,15 @@ bool database::fill_order( const limit_order_object& order, const asset& pays, c } return false; } -} +} FC_CAPTURE_AND_RETHROW( (order)(pays)(receives) ) } bool database::fill_order( const call_order_object& order, const asset& pays, const asset& receives ) { try { //idump((pays)(receives)(order)); - assert( order.get_debt().asset_id == receives.asset_id ); - assert( order.get_collateral().asset_id == pays.asset_id ); - assert( order.get_collateral() >= pays ); + FC_ASSERT( order.get_debt().asset_id == receives.asset_id ); + FC_ASSERT( order.get_collateral().asset_id == pays.asset_id ); + FC_ASSERT( order.get_collateral() >= pays ); optional collateral_freed; modify( order, [&]( call_order_object& o ){ @@ -315,11 +328,13 @@ bool database::fill_order( const call_order_object& order, const asset& pays, co const account_statistics_object& borrower_statistics = borrower.statistics(*this); if( collateral_freed ) adjust_balance(borrower.get_id(), *collateral_freed); + modify( borrower_statistics, [&]( account_statistics_object& b ){ if( collateral_freed && collateral_freed->amount > 0 ) b.total_core_in_orders -= collateral_freed->amount; if( pays.asset_id == asset_id_type() ) b.total_core_in_orders -= pays.amount; + assert( b.total_core_in_orders >= 0 ); }); } @@ -374,6 +389,10 @@ bool database::fill_order(const force_settlement_object& settle, const asset& pa bool database::check_call_orders(const asset_object& mia, bool enable_black_swan) { try { if( !mia.is_market_issued() ) return false; + + if( check_for_blackswan( mia, enable_black_swan ) ) + return false; + const asset_bitasset_data_object& bitasset = mia.bitasset_data(*this); if( bitasset.is_prediction_market ) return false; if( bitasset.current_feed.settlement_price.is_null() ) return false; @@ -395,15 +414,23 @@ bool database::check_call_orders(const asset_object& mia, bool enable_black_swan auto limit_end = limit_price_index.upper_bound( min_price ); if( limit_itr == limit_end ) { + /* + if( head_block_num() > 300000 ) + ilog( "no orders below between: ${p} and: ${m}", + ("p", bitasset.current_feed.max_short_squeeze_price()) + ("m", max_price) ); + */ return false; } - 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_min = price::min( bitasset.options.short_backing_asset, mia.id ); + auto call_max = price::max( bitasset.options.short_backing_asset, mia.id ); + auto call_itr = call_price_index.lower_bound( call_min ); + auto call_end = call_price_index.upper_bound( call_max ); bool filled_limit = false; - while( call_itr != call_end ) + while( !check_for_blackswan( mia, enable_black_swan ) && call_itr != call_end ) { bool filled_call = false; price match_price; @@ -419,17 +446,16 @@ bool database::check_call_orders(const asset_object& mia, bool enable_black_swan match_price.validate(); if( match_price > ~call_itr->call_price ) - { return filled_limit; - } auto usd_to_buy = call_itr->get_debt(); if( usd_to_buy * match_price > call_itr->get_collateral() ) { + elog( "black swan detected" ); + edump((enable_black_swan)); FC_ASSERT( enable_black_swan ); - //globally_settle_asset(mia, call_itr->get_debt() / call_itr->get_collateral()); - globally_settle_asset(mia, bitasset.current_feed.settlement_price );// call_itr->get_debt() / call_itr->get_collateral()); + globally_settle_asset(mia, bitasset.current_feed.settlement_price ); return true; } @@ -458,6 +484,7 @@ bool database::check_call_orders(const asset_object& mia, bool enable_black_swan auto old_limit_itr = filled_limit ? limit_itr++ : limit_itr; fill_order(*old_limit_itr, order_pays, order_receives); + } // whlie call_itr != call_end return filled_limit; @@ -468,7 +495,9 @@ void database::pay_order( const account_object& receiver, const asset& receives, const auto& balances = receiver.statistics(*this); modify( balances, [&]( account_statistics_object& b ){ if( pays.asset_id == asset_id_type() ) + { b.total_core_in_orders -= pays.amount; + } }); adjust_balance(receiver.get_id(), receives); } diff --git a/libraries/chain/db_update.cpp b/libraries/chain/db_update.cpp index 8b355cb9f0..bf030dbadc 100644 --- a/libraries/chain/db_update.cpp +++ b/libraries/chain/db_update.cpp @@ -143,6 +143,73 @@ void database::clear_expired_proposals() } } +/** + * let HB = the highest bid for the collateral (aka who will pay the most DEBT for the least collateral) + * let SP = current median feed's Settlement Price + * let LC = the least collateralized call order's swan price (debt/collateral) + * + * If there is no valid price feed or no bids then there is no black swan. + * + * A black swan occurs if MAX(HB,SP) <= LC + */ +bool database::check_for_blackswan( const asset_object& mia, bool enable_black_swan ) +{ + if( !mia.is_market_issued() ) return false; + + const asset_bitasset_data_object& bitasset = mia.bitasset_data(*this); + if( bitasset.has_settlement() ) return true; // already force settled + + const call_order_index& call_index = get_index_type(); + const auto& call_price_index = call_index.indices().get(); + + const limit_order_index& limit_index = get_index_type(); + const auto& limit_price_index = limit_index.indices().get(); + + // looking for limit orders selling the most USD for the least CORE + auto highest_possible_bid = price::max( mia.id, bitasset.options.short_backing_asset ); + // stop when limit orders are selling too little USD for too much CORE + auto lowest_possible_bid = price::min( mia.id, bitasset.options.short_backing_asset ); + + assert( highest_possible_bid.base.asset_id == lowest_possible_bid.base.asset_id ); + // NOTE limit_price_index is sorted from greatest to least + auto limit_itr = limit_price_index.lower_bound( highest_possible_bid ); + auto limit_end = limit_price_index.upper_bound( lowest_possible_bid ); + + auto call_min = price::min( bitasset.options.short_backing_asset, mia.id ); + auto call_max = price::max( bitasset.options.short_backing_asset, mia.id ); + auto call_itr = call_price_index.lower_bound( call_min ); + auto call_end = call_price_index.upper_bound( call_max ); + + auto settle_price = bitasset.current_feed.settlement_price; + + if( settle_price.is_null() ) return false; /// no feed; + if( call_itr == call_end ) return false; /// no call orders + + price highest = settle_price; + if( limit_itr != limit_end ) { + assert( settle_price.base.asset_id == limit_itr->sell_price.base.asset_id ); + highest = std::max( limit_itr->sell_price, settle_price ); + } + + auto least_collateral = call_itr->collateralization(); + if( ~least_collateral >= highest ) + { + elog( "Black Swan detected: \n" + " Least collateralized call: ${lc} ${~lc}\n" + // " Highest Bid: ${hb} ${~hb}\n" + " Settle Price: ${sp} ${~sp}\n" + " Max: ${h} ${~h}\n", + ("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()) ); + FC_ASSERT( enable_black_swan, "Black swan was detected during a margin update which is not allowed to trigger a blackswan" ); + globally_settle_asset(mia, ~least_collateral ); + return true; + } + return false; +} + void database::clear_expired_orders() { detail::with_skip_flags( *this, @@ -187,6 +254,13 @@ void database::clear_expired_orders() const asset_object& mia_object = get(current_asset); const asset_bitasset_data_object mia = mia_object.bitasset_data(*this); + if( mia.has_settlement() ) + { + ilog( "Canceling a force settlement because of black swan" ); + cancel_order( order ); + continue; + } + // Has this order not reached its settlement date? if( order.settlement_date > head_block_time() ) { @@ -234,7 +308,15 @@ void database::clear_expired_orders() // There should always be a call order, since asset exists! assert(itr != call_index.end() && itr->debt_type() == mia_object.get_id()); asset max_settlement = max_settlement_volume - settled; - settled += match(*itr, order, settlement_price, max_settlement); + + try { + settled += match(*itr, order, settlement_price, max_settlement); + } + catch ( const black_swan_exception& e ) { + wlog( "black swan detected: ${e}", ("e", e.to_detail_string() ) ); + cancel_order( order ); + break; + } } modify(mia, [settled](asset_bitasset_data_object& b) { b.force_settled_volume = settled.amount; diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index e5a436efe0..c099e1ef05 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -428,6 +428,7 @@ namespace graphene { namespace chain { void update_expired_feeds(); void update_maintenance_flag( bool new_maintenance_flag ); void update_withdraw_permissions(); + bool check_for_blackswan( const asset_object& mia, bool enable_black_swan = true ); ///Steps performed only at maintenance intervals ///@{ diff --git a/libraries/chain/include/graphene/chain/exceptions.hpp b/libraries/chain/include/graphene/chain/exceptions.hpp index 7bdc7ca50b..3244236edb 100644 --- a/libraries/chain/include/graphene/chain/exceptions.hpp +++ b/libraries/chain/include/graphene/chain/exceptions.hpp @@ -69,7 +69,8 @@ namespace graphene { namespace chain { FC_DECLARE_DERIVED_EXCEPTION( operation_evaluate_exception, graphene::chain::chain_exception, 3050000, "operation evaluation exception" ) FC_DECLARE_DERIVED_EXCEPTION( utility_exception, graphene::chain::chain_exception, 3060000, "utility method exception" ) FC_DECLARE_DERIVED_EXCEPTION( undo_database_exception, graphene::chain::chain_exception, 3070000, "undo database exception" ) - FC_DECLARE_DERIVED_EXCEPTION( unlinkable_block_exception, graphene::chain::chain_exception, 3080000, "unlinkable block" ) + FC_DECLARE_DERIVED_EXCEPTION( unlinkable_block_exception, graphene::chain::chain_exception, 3080000, "unlinkable block" ) + FC_DECLARE_DERIVED_EXCEPTION( black_swan_exception, graphene::chain::chain_exception, 3090000, "black swan" ) FC_DECLARE_DERIVED_EXCEPTION( tx_missing_active_auth, graphene::chain::transaction_exception, 3030001, "missing required active authority" ) FC_DECLARE_DERIVED_EXCEPTION( tx_missing_owner_auth, graphene::chain::transaction_exception, 3030002, "missing required owner authority" ) diff --git a/libraries/chain/market_evaluator.cpp b/libraries/chain/market_evaluator.cpp index 786a56ad47..77f15cf6af 100644 --- a/libraries/chain/market_evaluator.cpp +++ b/libraries/chain/market_evaluator.cpp @@ -183,6 +183,8 @@ 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); + + auto swan_price = call.get_debt()/ call.get_collateral(); }); } else @@ -194,6 +196,7 @@ void_result call_order_update_evaluator::do_apply(const call_order_update_operat call.debt += o.delta_debt.amount; if( call.debt > 0 ) { + auto swan_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); } @@ -229,6 +232,7 @@ void_result call_order_update_evaluator::do_apply(const call_order_update_operat } else { + //edump( (~call_obj->call_price) ("<")( _bitasset_data->current_feed.settlement_price) ); // We didn't fill any call orders. This may be because we // aren't in margin call territory, or it may be because there // were no matching orders. In the latter case, we throw.