From 14a3e7d9e963db7cb414cb18e368d395f2fed467 Mon Sep 17 00:00:00 2001 From: sierra19XX <15652887+sierra19XX@users.noreply.github.com> Date: Mon, 22 Mar 2021 03:04:32 +1100 Subject: [PATCH] 518 583 868 890 922 931 935 1270 hf fixes --- libraries/chain/asset_evaluator.cpp | 415 ++++++++++++++++-- libraries/chain/asset_object.cpp | 15 +- libraries/chain/db_maint.cpp | 283 +++++++++++- libraries/chain/db_market.cpp | 217 +++++---- libraries/chain/db_update.cpp | 49 ++- libraries/chain/hardfork.d/CORE_1270.hf | 4 + libraries/chain/hardfork.d/CORE_518.hf | 4 + libraries/chain/hardfork.d/CORE_583.hf | 4 + libraries/chain/hardfork.d/CORE_868_890.hf | 5 + libraries/chain/hardfork.d/CORE_922_931.hf | 5 + libraries/chain/hardfork.d/CORE_935.hf | 4 + .../graphene/chain/asset_evaluator.hpp | 1 + .../include/graphene/chain/asset_object.hpp | 18 +- .../chain/include/graphene/chain/database.hpp | 4 +- .../include/graphene/chain/market_object.hpp | 16 +- .../include/graphene/chain/protocol/asset.hpp | 16 +- libraries/chain/market_evaluator.cpp | 64 ++- libraries/chain/market_object.cpp | 54 ++- libraries/chain/protocol/asset.cpp | 33 +- tests/common/database_fixture.cpp | 32 ++ tests/common/database_fixture.hpp | 16 + 21 files changed, 1066 insertions(+), 193 deletions(-) create mode 100644 libraries/chain/hardfork.d/CORE_1270.hf create mode 100644 libraries/chain/hardfork.d/CORE_518.hf create mode 100644 libraries/chain/hardfork.d/CORE_583.hf create mode 100644 libraries/chain/hardfork.d/CORE_868_890.hf create mode 100644 libraries/chain/hardfork.d/CORE_922_931.hf create mode 100644 libraries/chain/hardfork.d/CORE_935.hf diff --git a/libraries/chain/asset_evaluator.cpp b/libraries/chain/asset_evaluator.cpp index 5b7bc2a2..d4109f35 100644 --- a/libraries/chain/asset_evaluator.cpp +++ b/libraries/chain/asset_evaluator.cpp @@ -481,7 +481,7 @@ void_result asset_update_evaluator::do_apply(const asset_update_operation& o) database& d = db(); // If we are now disabling force settlements, cancel all open force settlement orders - if( o.new_options.flags & disable_force_settle && asset_to_update->can_force_settle() ) + if( (o.new_options.flags & disable_force_settle) && asset_to_update->can_force_settle() ) { const auto& idx = d.get_index_type().indices().get(); // Funky iteration code because we're removing objects as we go. We have to re-initialize itr every loop instead @@ -515,57 +515,366 @@ void_result asset_update_evaluator::do_apply(const asset_update_operation& o) return void_result(); } FC_CAPTURE_AND_RETHROW( (o) ) } -void_result asset_update_bitasset_evaluator::do_evaluate(const asset_update_bitasset_operation& o) +/**************** + * Loop through assets, looking for ones that are backed by the asset being changed. When found, + * perform checks to verify validity + * + * @param d the database + * @param op the bitasset update operation being performed + * @param new_backing_asset + * @param true if after hf 922/931 (if nothing triggers, this and the logic that depends on it + * should be removed). + */ +void check_children_of_bitasset(database& d, const asset_update_bitasset_operation& op, + const asset_object& new_backing_asset, bool after_hf_922_931) +{ + // no need to do these checks if the new backing asset is CORE + if ( new_backing_asset.get_id() == asset_id_type() ) + return; + + // loop through all assets that have this asset as a backing asset + const auto& idx = d.get_index_type().indices().get(); + + for( auto itr = idx.lower_bound(true); itr != idx.end(); ++itr ) + { + const auto& child = *itr; + if ( child.bitasset_data(d).options.short_backing_asset == op.asset_to_update ) + { + if ( after_hf_922_931 ) + { + FC_ASSERT( child.get_id() != op.new_options.short_backing_asset, + "A BitAsset would be invalidated by changing this backing asset ('A' backed by 'B' backed by 'A')." ); + + FC_ASSERT( child.issuer != GRAPHENE_COMMITTEE_ACCOUNT, + "A blockchain-controlled market asset would be invalidated by changing this backing asset." ); + + FC_ASSERT( !new_backing_asset.is_market_issued(), + "A non-blockchain controlled BitAsset would be invalidated by changing this backing asset."); + + } + else + { + if( child.get_id() == op.new_options.short_backing_asset ) + { + wlog( "Before hf-922-931, modified an asset to be backed by another, but would cause a continuous " + "loop. A cannot be backed by B which is backed by A." ); + return; + } + + if( child.issuer == GRAPHENE_COMMITTEE_ACCOUNT ) + { + wlog( "before hf-922-931, modified an asset to be backed by a non-CORE, but this asset " + "is a backing asset for a committee-issued asset. This occurred at block ${b}", + ("b", d.head_block_num())); + return; + } + else + { + if ( new_backing_asset.is_market_issued() ) { // a.k.a. !UIA + wlog( "before hf-922-931, modified an asset to be backed by an MPA, but this asset " + "is a backing asset for another MPA, which would cause MPA backed by MPA backed by MPA. " + "This occurred at block ${b}", + ("b", d.head_block_num())); + return; + } + } // if child.issuer + } // if hf 922/931 + } // if this child is backed by the asset being adjusted + } // for each asset +} // check_children_of_bitasset + +void_result asset_update_bitasset_evaluator::do_evaluate(const asset_update_bitasset_operation& op) { try { database& d = db(); - const asset_object& a = o.asset_to_update(d); + const asset_object& asset_obj = op.asset_to_update(d); - FC_ASSERT(a.is_market_issued(), "Cannot update BitAsset-specific settings on a non-BitAsset."); + FC_ASSERT( asset_obj.is_market_issued(), "Cannot update BitAsset-specific settings on a non-BitAsset." ); - const asset_bitasset_data_object& b = a.bitasset_data(d); - FC_ASSERT( !b.has_settlement(), "Cannot update a bitasset after a settlement has executed" ); - if( o.new_options.short_backing_asset != b.options.short_backing_asset ) + FC_ASSERT( op.issuer == asset_obj.issuer, "Only asset issuer can update bitasset_data of the asset." ); + + const asset_bitasset_data_object& current_bitasset_data = asset_obj.bitasset_data(d); + + FC_ASSERT( !current_bitasset_data.has_settlement(), "Cannot update a bitasset after a global settlement has executed" ); + + bool after_hf_core_922_931 = ( d.get_dynamic_global_properties().next_maintenance_time > HARDFORK_CORE_922_931_TIME ); + + // Are we changing the backing asset? + if( op.new_options.short_backing_asset != current_bitasset_data.options.short_backing_asset ) { - FC_ASSERT(a.dynamic_asset_data_id(d).current_supply == 0); - FC_ASSERT(d.find_object(o.new_options.short_backing_asset)); + FC_ASSERT( asset_obj.dynamic_asset_data_id(d).current_supply == 0, + "Cannot update a bitasset if there is already a current supply." ); - if( a.issuer == GRAPHENE_COMMITTEE_ACCOUNT ) + const asset_object& new_backing_asset = op.new_options.short_backing_asset(d); // check if the asset exists + + if( after_hf_core_922_931 ) // TODO remove this check after hard fork if things in `else` did not occur { - const asset_object& backing = a.bitasset_data(d).options.short_backing_asset(d); - if( backing.is_market_issued() ) + FC_ASSERT( op.new_options.short_backing_asset != asset_obj.get_id(), + "Cannot update an asset to be backed by itself." ); + + if( current_bitasset_data.is_prediction_market ) { - const asset_object& backing_backing = backing.bitasset_data(d).options.short_backing_asset(d); - FC_ASSERT( backing_backing.get_id() == asset_id_type(), - "May not create a blockchain-controlled market asset which is not backed by CORE."); - } else - FC_ASSERT( backing.get_id() == asset_id_type(), - "May not create a blockchain-controlled market asset which is not backed by CORE."); + FC_ASSERT( asset_obj.precision == new_backing_asset.precision, + "The precision of the asset and backing asset must be equal." ); + } + + if( asset_obj.issuer == GRAPHENE_COMMITTEE_ACCOUNT ) + { + if( new_backing_asset.is_market_issued() ) + { + FC_ASSERT( new_backing_asset.bitasset_data(d).options.short_backing_asset == asset_id_type(), + "May not modify a blockchain-controlled market asset to be backed by an asset which is not " + "backed by CORE." ); + + check_children_of_bitasset( d, op, new_backing_asset, after_hf_core_922_931 ); + } + else + { + FC_ASSERT( new_backing_asset.get_id() == asset_id_type(), + "May not modify a blockchain-controlled market asset to be backed by an asset which is not " + "market issued asset nor CORE." ); + } + } + else + { + // not a committee issued asset + + // If we're changing to a backing_asset that is not CORE, we need to look at any + // asset ( "CHILD" ) that has this one as a backing asset. If CHILD is committee-owned, + // the change is not allowed. If CHILD is user-owned, then this asset's backing + // asset must be either CORE or a UIA. + if ( new_backing_asset.get_id() != asset_id_type() ) // not backed by CORE + { + check_children_of_bitasset( d, op, new_backing_asset, after_hf_core_922_931 ); + } + + } + + // Check if the new backing asset is itself backed by something. It must be CORE or a UIA + if ( new_backing_asset.is_market_issued() ) + { + const asset_object& backing_backing_asset = new_backing_asset.bitasset_data(d).asset_id(d); + FC_ASSERT( !backing_backing_asset.is_market_issued(), "A BitAsset cannot be backed by a BitAsset that itself " + "is backed by a BitAsset."); + } + } + else // prior to HF 922 / 931 + { + // code to check if issues occurred before hard fork. TODO cleanup after hard fork + if( op.new_options.short_backing_asset == asset_obj.get_id() ) + { + wlog( "before hf-922-931, op.new_options.short_backing_asset == asset_obj.get_id() at block ${b}", + ("b",d.head_block_num()) ); + } + if( current_bitasset_data.is_prediction_market && asset_obj.precision != new_backing_asset.precision ) + { + wlog( "before hf-922-931, for a PM, asset_obj.precision != new_backing_asset.precision at block ${b}", + ("b",d.head_block_num()) ); + } + + if( asset_obj.issuer == GRAPHENE_COMMITTEE_ACCOUNT ) + { + // code to check if issues occurred before hard fork. TODO cleanup after hard fork + if( new_backing_asset.is_market_issued() ) + { + if( new_backing_asset.bitasset_data(d).options.short_backing_asset != asset_id_type() ) + wlog( "before hf-922-931, modified a blockchain-controlled market asset to be backed by an asset " + "which is not backed by CORE at block ${b}", + ("b",d.head_block_num()) ); + + check_children_of_bitasset( d, op, new_backing_asset, after_hf_core_922_931 ); + } + else + { + if( new_backing_asset.get_id() != asset_id_type() ) + wlog( "before hf-922-931, modified a blockchain-controlled market asset to be backed by an asset " + "which is not market issued asset nor CORE at block ${b}", + ("b",d.head_block_num()) ); + } + + //prior to HF 922_931, these checks were mistakenly using the old backing_asset + const asset_object& old_backing_asset = current_bitasset_data.options.short_backing_asset(d); + + if( old_backing_asset.is_market_issued() ) + { + FC_ASSERT( old_backing_asset.bitasset_data(d).options.short_backing_asset == asset_id_type(), + "May not modify a blockchain-controlled market asset to be backed by an asset which is not " + "backed by CORE." ); + } + else + { + FC_ASSERT( old_backing_asset.get_id() == asset_id_type(), + "May not modify a blockchain-controlled market asset to be backed by an asset which is not " + "market issued asset nor CORE." ); + } + } + else + { + // not a committee issued asset + + // If we're changing to a backing_asset that is not CORE, we need to look at any + // asset ( "CHILD" ) that has this one as a backing asset. If CHILD is committee-owned, + // the change is not allowed. If CHILD is user-owned, then this asset's backing + // asset must be either CORE or a UIA. + if ( new_backing_asset.get_id() != asset_id_type() ) // not backed by CORE + { + check_children_of_bitasset( d, op, new_backing_asset, after_hf_core_922_931 ); + } + } + // if the new backing asset is backed by something which is not CORE and not a UIA, this is not allowed + // Check if the new backing asset is itself backed by something. It must be CORE or a UIA + if ( new_backing_asset.is_market_issued() ) + { + const asset_object& backing_backing_asset = new_backing_asset.bitasset_data(d).asset_id(d); + if ( backing_backing_asset.get_id() != asset_id_type() ) + { + if ( backing_backing_asset.is_market_issued() ) + { + wlog( "before hf-922-931, a BitAsset cannot be backed by a BitAsset that itself " + "is backed by a BitAsset. This occurred at block ${b}", + ("b", d.head_block_num() ) ); + } + } // not core + } // if market issued } } - bitasset_to_update = &b; - FC_ASSERT( o.issuer == a.issuer, "", ("o.issuer", o.issuer)("a.issuer", a.issuer) ); + const auto& chain_parameters = d.get_global_properties().parameters; + if( after_hf_core_922_931 ) // TODO remove this check after hard fork if things in `else` did not occur + { + FC_ASSERT( op.new_options.feed_lifetime_sec > chain_parameters.block_interval, + "Feed lifetime must exceed block interval." ); + FC_ASSERT( op.new_options.force_settlement_delay_sec > chain_parameters.block_interval, + "Force settlement delay must exceed block interval." ); + } + else // code to check if issues occurred before hard fork. TODO cleanup after hard fork + { + if( op.new_options.feed_lifetime_sec <= chain_parameters.block_interval ) + wlog( "before hf-922-931, op.new_options.feed_lifetime_sec <= chain_parameters.block_interval at block ${b}", + ("b",d.head_block_num()) ); + if( op.new_options.force_settlement_delay_sec <= chain_parameters.block_interval ) + wlog( "before hf-922-931, op.new_options.force_settlement_delay_sec <= chain_parameters.block_interval at block ${b}", + ("b",d.head_block_num()) ); + } + + bitasset_to_update = ¤t_bitasset_data; + asset_to_update = &asset_obj; return void_result(); -} FC_CAPTURE_AND_RETHROW( (o) ) } +} FC_CAPTURE_AND_RETHROW( (op) ) } -void_result asset_update_bitasset_evaluator::do_apply(const asset_update_bitasset_operation& o) -{ try { - bool should_update_feeds = false; +/******* + * @brief Apply requested changes to bitasset options + * + * This applies the requested changes to the bitasset object. It also cleans up the + * releated feeds + * + * @param op the requested operation + * @param db the database + * @param bdo the actual database object + * @param asset_to_update the asset_object related to this bitasset_data_object + * @returns true if the feed price is changed, and after hf core-868-890 + */ +static bool update_bitasset_object_options( + const asset_update_bitasset_operation& op, database& db, + asset_bitasset_data_object& bdo, const asset_object& asset_to_update ) +{ + const fc::time_point_sec& next_maint_time = db.get_dynamic_global_properties().next_maintenance_time; + bool after_hf_core_868_890 = ( next_maint_time > HARDFORK_CORE_868_890_TIME ); // If the minimum number of feeds to calculate a median has changed, we need to recalculate the median - if( o.new_options.minimum_feeds != bitasset_to_update->options.minimum_feeds ) + bool should_update_feeds = false; + if( op.new_options.minimum_feeds != bdo.options.minimum_feeds ) should_update_feeds = true; - db().modify(*bitasset_to_update, [&](asset_bitasset_data_object& b) { - b.options = o.new_options; + // after hardfork core-868-890, we also should call update_median_feeds if the feed_lifetime_sec changed + if( after_hf_core_868_890 + && op.new_options.feed_lifetime_sec != bdo.options.feed_lifetime_sec ) + { + should_update_feeds = true; + } - if( should_update_feeds ) - b.update_median_feeds(db().head_block_time()); - }); + // feeds must be reset if the backing asset is changed after hardfork core-868-890 + bool backing_asset_changed = false; + bool is_witness_or_committee_fed = false; + if( after_hf_core_868_890 + && op.new_options.short_backing_asset != bdo.options.short_backing_asset ) + { + backing_asset_changed = true; + should_update_feeds = true; + if ( asset_to_update.options.flags & (witness_fed_asset | committee_fed_asset) ) + is_witness_or_committee_fed = true; + } - return void_result(); -} FC_CAPTURE_AND_RETHROW( (o) ) } + bdo.options = op.new_options; + + // are we modifying the underlying? If so, reset the feeds + if (backing_asset_changed) + { + if ( is_witness_or_committee_fed ) + { + bdo.feeds.clear(); + } + else + { + // for non-witness-feeding and non-committee-feeding assets, modify all feeds + // published by producers to nothing, since we can't simply remove them. For more information: + // https://github.com/bitshares/bitshares-core/pull/832#issuecomment-384112633 + for(auto& current_feed : bdo.feeds) + { + current_feed.second.second.settlement_price = price(); + } + } + } + + if( should_update_feeds ) + { + const auto old_feed = bdo.current_feed; + bdo.update_median_feeds( db.head_block_time(), next_maint_time ); + + // TODO review and refactor / cleanup after hard fork: + // 1. if hf_core_868_890 and core-935 occurred at same time + // 2. if wlog did not actually get called + + // We need to call check_call_orders if the price feed changes after hardfork core-935 + if( next_maint_time > HARDFORK_CORE_935_TIME ) + return ( !( old_feed == bdo.current_feed ) ); + + // We need to call check_call_orders if the settlement price changes after hardfork core-868-890 + if( after_hf_core_868_890 ) + { + if( old_feed.settlement_price != bdo.current_feed.settlement_price ) + return true; + else + { + if( !( old_feed == bdo.current_feed ) ) + wlog( "Settlement price did not change but current_feed changed at block ${b}", ("b",db.head_block_num()) ); + } + } + } + + return false; +} + +void_result asset_update_bitasset_evaluator::do_apply(const asset_update_bitasset_operation& op) +{ + try + { + auto& db_conn = db(); + const auto& asset_being_updated = (*asset_to_update); + bool to_check_call_orders = false; + + db_conn.modify( *bitasset_to_update, + [&op, &asset_being_updated, &to_check_call_orders, &db_conn]( asset_bitasset_data_object& bdo ) + { + to_check_call_orders = update_bitasset_object_options( op, db_conn, bdo, asset_being_updated ); + }); + + if( to_check_call_orders ) + db_conn.check_call_orders( asset_being_updated ); + + return void_result(); + } FC_CAPTURE_AND_RETHROW( (op) ) +} void_result asset_update_dividend_evaluator::do_evaluate(const asset_update_dividend_operation& o) { try { @@ -652,6 +961,7 @@ void_result asset_update_feed_producers_evaluator::do_evaluate(const asset_updat void_result asset_update_feed_producers_evaluator::do_apply(const asset_update_feed_producers_evaluator::operation_type& o) { try { + const auto next_maint_time = db().get_dynamic_global_properties().next_maintenance_time; db().modify(*bitasset_to_update, [&](asset_bitasset_data_object& a) { //This is tricky because I have a set of publishers coming in, but a map of publisher to feed is stored. //I need to update the map such that the keys match the new publishers, but not munge the old price feeds from @@ -668,7 +978,7 @@ void_result asset_update_feed_producers_evaluator::do_apply(const asset_update_f for( auto itr = o.new_feed_producers.begin(); itr != o.new_feed_producers.end(); ++itr ) if( !a.feeds.count(*itr) ) a.feeds[*itr]; - a.update_median_feeds(db().head_block_time()); + a.update_median_feeds(db().head_block_time(), next_maint_time); }); db().check_call_orders( o.asset_to_update(db()) ); @@ -835,27 +1145,48 @@ void_result asset_publish_feeds_evaluator::do_apply(const asset_publish_feed_ope { try { database& d = db(); + const auto head_time = d.head_block_time(); + const auto next_maint_time = d.get_dynamic_global_properties().next_maintenance_time; const asset_object& base = o.asset_id(d); const asset_bitasset_data_object& bad = base.bitasset_data(d); auto old_feed = bad.current_feed; // Store medians for this asset - d.modify(bad , [&o,&d](asset_bitasset_data_object& a) { - a.feeds[o.publisher] = make_pair(d.head_block_time(), o.feed); - a.update_median_feeds(d.head_block_time()); + d.modify( bad , [&o,head_time,next_maint_time](asset_bitasset_data_object& a) { + a.feeds[o.publisher] = make_pair( head_time, o.feed ); + a.update_median_feeds( head_time, next_maint_time ); }); if( !(old_feed == bad.current_feed) ) { - if( bad.has_settlement() ) // implies head_block_time > HARDFORK_CORE_216_TIME + // Check whether need to revive the asset and proceed if need + if( bad.has_settlement() // has globally settled, implies head_block_time > HARDFORK_CORE_216_TIME + && !bad.current_feed.settlement_price.is_null() ) // has a valid feed { + bool should_revive = false; const auto& mia_dyn = base.dynamic_asset_data_id(d); - if( !bad.current_feed.settlement_price.is_null() - && ( mia_dyn.current_supply == 0 - || ~price::call_price(asset(mia_dyn.current_supply, o.asset_id), - asset(bad.settlement_fund, bad.options.short_backing_asset), - bad.current_feed.maintenance_collateral_ratio ) < bad.current_feed.settlement_price ) ) + if( mia_dyn.current_supply == 0 ) // if current supply is zero, revive the asset + should_revive = true; + else // if current supply is not zero, when collateral ratio of settlement fund is greater than MCR, revive the asset + { + if( next_maint_time <= HARDFORK_CORE_1270_TIME ) + { + // before core-1270 hard fork, calculate call_price and compare to median feed + if( ~price::call_price( asset(mia_dyn.current_supply, o.asset_id), + asset(bad.settlement_fund, bad.options.short_backing_asset), + bad.current_feed.maintenance_collateral_ratio ) < bad.current_feed.settlement_price ) + should_revive = true; + } + else + { + // after core-1270 hard fork, calculate collateralization and compare to maintenance_collateralization + if( price( asset( bad.settlement_fund, bad.options.short_backing_asset ), + asset( mia_dyn.current_supply, o.asset_id ) ) > bad.current_maintenance_collateralization ) + should_revive = true; + } + } + if( should_revive ) d.revive_bitasset(base); } db().check_call_orders(base); diff --git a/libraries/chain/asset_object.cpp b/libraries/chain/asset_object.cpp index 70adbde8..71196866 100644 --- a/libraries/chain/asset_object.cpp +++ b/libraries/chain/asset_object.cpp @@ -44,10 +44,13 @@ share_type asset_bitasset_data_object::max_force_settlement_volume(share_type cu return volume.to_uint64(); } -void asset_bitasset_data_object::update_median_feeds(time_point_sec current_time) +void graphene::chain::asset_bitasset_data_object::update_median_feeds( time_point_sec current_time, + time_point_sec next_maintenance_time ) { + bool after_core_hardfork_1270 = ( next_maintenance_time > HARDFORK_CORE_1270_TIME ); // call price caching issue current_feed_publication_time = current_time; vector> current_feeds; + // find feeds that were alive at current_time for( const pair>& f : feeds ) { if( (current_time - f.second.first).to_seconds() < options.feed_lifetime_sec && @@ -65,13 +68,18 @@ void asset_bitasset_data_object::update_median_feeds(time_point_sec current_time feed_cer_updated = false; // new median cer is null, won't update asset_object anyway, set to false for better performance current_feed_publication_time = current_time; current_feed = price_feed(); + if( after_core_hardfork_1270 ) + current_maintenance_collateralization = price(); return; } if( current_feeds.size() == 1 ) { if( current_feed.core_exchange_rate != current_feeds.front().get().core_exchange_rate ) feed_cer_updated = true; - current_feed = std::move(current_feeds.front()); + current_feed = current_feeds.front(); + // Note: perhaps can defer updating current_maintenance_collateralization for better performance + if( after_core_hardfork_1270 ) + current_maintenance_collateralization = current_feed.maintenance_collateralization(); return; } @@ -92,6 +100,9 @@ void asset_bitasset_data_object::update_median_feeds(time_point_sec current_time if( current_feed.core_exchange_rate != median_feed.core_exchange_rate ) feed_cer_updated = true; current_feed = median_feed; + // Note: perhaps can defer updating current_maintenance_collateralization for better performance + if( after_core_hardfork_1270 ) + current_maintenance_collateralization = current_feed.maintenance_collateralization(); } diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index d923c1d0..134c305f 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -1939,7 +1939,9 @@ void database::perform_son_tasks() } } -void update_and_match_call_orders( database& db ) +/// Reset call_price of all call orders according to their remaining collateral and debt. +/// Do not update orders of prediction markets because we're sure they're up to date. +void update_call_orders_hf_343( database& db ) { // Update call_price wlog( "Updating all call orders for hardfork core-343 at block ${n}", ("n",db.head_block_num()) ); @@ -1960,7 +1962,30 @@ void update_and_match_call_orders( database& db ) abd->current_feed.maintenance_collateral_ratio ); }); } + wlog( "Done updating all call orders for hardfork core-343 at block ${n}", ("n",db.head_block_num()) ); +} + +/// Reset call_price of all call orders to (1,1) since it won't be used in the future. +/// Update PMs as well. +void update_call_orders_hf_1270( database& db ) +{ + // Update call_price + wlog( "Updating all call orders for hardfork core-1270 at block ${n}", ("n",db.head_block_num()) ); + for( const auto& call_obj : db.get_index_type().indices().get() ) + { + db.modify( call_obj, []( call_order_object& call ) { + call.call_price.base.amount = 1; + call.call_price.quote.amount = 1; + }); + } + wlog( "Done updating all call orders for hardfork core-1270 at block ${n}", ("n",db.head_block_num()) ); +} + +/// Match call orders for all bitAssets, including PMs. +void match_call_orders( database& db ) +{ // Match call orders + wlog( "Matching call orders at block ${n}", ("n",db.head_block_num()) ); const auto& asset_idx = db.get_index_type().indices().get(); auto itr = asset_idx.lower_bound( true /** market issued */ ); while( itr != asset_idx.end() ) @@ -1970,7 +1995,7 @@ void update_and_match_call_orders( database& db ) // be here, next_maintenance_time should have been updated already db.check_call_orders( a, true, false ); // allow black swan, and call orders are taker } - wlog( "Done updating all call orders for hardfork core-343 at block ${n}", ("n",db.head_block_num()) ); + wlog( "Done matching call orders at block ${n}", ("n",db.head_block_num()) ); } void database::process_bids( const asset_bitasset_data_object& bad ) @@ -2024,6 +2049,216 @@ void database::process_bids( const asset_bitasset_data_object& bad ) _cancel_bids_and_revive_mpa( to_revive, bad ); } +void update_median_feeds(database& db) +{ + time_point_sec head_time = db.head_block_time(); + time_point_sec next_maint_time = db.get_dynamic_global_properties().next_maintenance_time; + + const auto update_bitasset = [head_time, next_maint_time]( asset_bitasset_data_object &o ) + { + o.update_median_feeds( head_time, next_maint_time ); + }; + + for( const auto& d : db.get_index_type().indices() ) + { + db.modify( d, update_bitasset ); + } +} + +/****** + * @brief one-time data process for hard fork core-868-890 + * + * Prior to hardfork 868, switching a bitasset's shorting asset would not reset its + * feeds. This method will run at the hardfork time, and erase (or nullify) feeds + * that have incorrect backing assets. + * https://github.com/bitshares/bitshares-core/issues/868 + * + * Prior to hardfork 890, changing a bitasset's feed expiration time would not + * trigger a median feed update. This method will run at the hardfork time, and + * correct all median feed data. + * https://github.com/bitshares/bitshares-core/issues/890 + * + * @param db the database + * @param skip_check_call_orders true if check_call_orders() should not be called + */ +// TODO: for better performance, this function can be removed if it actually updated nothing at hf time. +// * Also need to update related test cases +// * NOTE: perhaps the removal can't be applied to testnet +void process_hf_868_890( database& db, bool skip_check_call_orders ) +{ + const auto next_maint_time = db.get_dynamic_global_properties().next_maintenance_time; + const auto head_time = db.head_block_time(); + const auto head_num = db.head_block_num(); + wlog( "Processing hard fork core-868-890 at block ${n}", ("n",head_num) ); + // for each market issued asset + const auto& asset_idx = db.get_index_type().indices().get(); + for( auto asset_itr = asset_idx.lower_bound(true); asset_itr != asset_idx.end(); ++asset_itr ) + { + const auto& current_asset = *asset_itr; + // Incorrect witness & committee feeds can simply be removed. + // For non-witness-fed and non-committee-fed assets, set incorrect + // feeds to price(), since we can't simply remove them. For more information: + // https://github.com/bitshares/bitshares-core/pull/832#issuecomment-384112633 + bool is_witness_or_committee_fed = false; + if ( current_asset.options.flags & ( witness_fed_asset | committee_fed_asset ) ) + is_witness_or_committee_fed = true; + + // for each feed + const asset_bitasset_data_object& bitasset_data = current_asset.bitasset_data(db); + // NOTE: We'll only need old_feed if HF343 hasn't rolled out yet + auto old_feed = bitasset_data.current_feed; + bool feeds_changed = false; // did any feed change + auto itr = bitasset_data.feeds.begin(); + while( itr != bitasset_data.feeds.end() ) + { + // If the feed is invalid + if ( itr->second.second.settlement_price.quote.asset_id != bitasset_data.options.short_backing_asset + && ( is_witness_or_committee_fed || itr->second.second.settlement_price != price() ) ) + { + feeds_changed = true; + db.modify( bitasset_data, [&itr, is_witness_or_committee_fed]( asset_bitasset_data_object& obj ) + { + if( is_witness_or_committee_fed ) + { + // erase the invalid feed + itr = obj.feeds.erase(itr); + } + else + { + // nullify the invalid feed + obj.feeds[itr->first].second.settlement_price = price(); + ++itr; + } + }); + } + else + { + // Feed is valid. Skip it. + ++itr; + } + } // end loop of each feed + + // if any feed was modified, print a warning message + if( feeds_changed ) + { + wlog( "Found invalid feed for asset ${asset_sym} (${asset_id}) during hardfork core-868-890", + ("asset_sym", current_asset.symbol)("asset_id", current_asset.id) ); + } + + // always update the median feed due to https://github.com/bitshares/bitshares-core/issues/890 + db.modify( bitasset_data, [head_time,next_maint_time]( asset_bitasset_data_object &obj ) { + obj.update_median_feeds( head_time, next_maint_time ); + }); + + bool median_changed = ( old_feed.settlement_price != bitasset_data.current_feed.settlement_price ); + bool median_feed_changed = ( !( old_feed == bitasset_data.current_feed ) ); + if( median_feed_changed ) + { + wlog( "Median feed for asset ${asset_sym} (${asset_id}) changed during hardfork core-868-890", + ("asset_sym", current_asset.symbol)("asset_id", current_asset.id) ); + } + // Note: due to bitshares-core issue #935, the check below (using median_changed) is incorrect. + // However, `skip_check_call_orders` will likely be true in both testnet and mainnet, + // so effectively the incorrect code won't make a difference. + // Additionally, we have code to update all call orders again during hardfork core-935 + // TODO cleanup after hard fork + if( !skip_check_call_orders && median_changed ) // check_call_orders should be called + { + db.check_call_orders( current_asset ); + } + else if( !skip_check_call_orders && median_feed_changed ) + { + wlog( "Incorrectly skipped check_call_orders for asset ${asset_sym} (${asset_id}) during hardfork core-868-890", + ("asset_sym", current_asset.symbol)("asset_id", current_asset.id) ); + } + } // for each market issued asset + wlog( "Done processing hard fork core-868-890 at block ${n}", ("n",head_num) ); +} + +/****** + * @brief one-time data process for hard fork core-935 + * + * Prior to hardfork 935, `check_call_orders` may be unintendedly skipped when + * median price feed has changed. This method will run at the hardfork time, and + * call `check_call_orders` for all markets. + * https://github.com/bitshares/bitshares-core/issues/935 + * + * @param db the database + */ +// TODO: for better performance, this function can be removed if it actually updated nothing at hf time. +// * Also need to update related test cases +// * NOTE: perhaps the removal can't be applied to testnet +void process_hf_935( database& db ) +{ + bool changed_something = false; + const asset_bitasset_data_object* bitasset = nullptr; + bool settled_before_check_call; + bool settled_after_check_call; + // for each market issued asset + const auto& asset_idx = db.get_index_type().indices().get(); + for( auto asset_itr = asset_idx.lower_bound(true); asset_itr != asset_idx.end(); ++asset_itr ) + { + const auto& current_asset = *asset_itr; + + if( !changed_something ) + { + bitasset = ¤t_asset.bitasset_data( db ); + settled_before_check_call = bitasset->has_settlement(); // whether already force settled + } + + bool called_some = db.check_call_orders( current_asset ); + + if( !changed_something ) + { + settled_after_check_call = bitasset->has_settlement(); // whether already force settled + + if( settled_before_check_call != settled_after_check_call || called_some ) + { + changed_something = true; + wlog( "process_hf_935 changed something" ); + } + } + } +} + +void database::process_bitassets() +{ + time_point_sec head_time = head_block_time(); + uint32_t head_epoch_seconds = head_time.sec_since_epoch(); + bool after_hf_core_518 = ( head_time >= HARDFORK_CORE_518_TIME ); // clear expired feeds + + const auto update_bitasset = [this,head_time,head_epoch_seconds,after_hf_core_518]( asset_bitasset_data_object &o ) + { + o.force_settled_volume = 0; // Reset all BitAsset force settlement volumes to zero + + // clear expired feeds + if( after_hf_core_518 ) + { + const auto &asset = get( o.asset_id ); + auto flags = asset.options.flags; + if ( ( flags & ( witness_fed_asset | committee_fed_asset ) ) && + o.options.feed_lifetime_sec < head_epoch_seconds ) // if smartcoin && check overflow + { + fc::time_point_sec calculated = head_time - o.options.feed_lifetime_sec; + for( auto itr = o.feeds.rbegin(); itr != o.feeds.rend(); ) // loop feeds + { + auto feed_time = itr->second.first; + std::advance( itr, 1 ); + if( feed_time < calculated ) + o.feeds.erase( itr.base() ); // delete expired feed + } + } + } + }; + + for( const auto& d : get_index_type().indices() ) + { + modify( d, update_bitasset ); + if( d.has_settlement() ) + process_bids(d); + } +} + void database::perform_chain_maintenance(const signed_block& next_block, const global_property_object& global_props) { try { const auto& gpo = get_global_properties(); @@ -2280,27 +2515,47 @@ void database::perform_chain_maintenance(const signed_block& next_block, const g if( (dgpo.next_maintenance_time < HARDFORK_613_TIME) && (next_maintenance_time >= HARDFORK_613_TIME) ) deprecate_annual_members(*this); - // To reset call_price of all call orders, then match by new rule - bool to_update_and_match_call_orders = false; + // To reset call_price of all call orders, then match by new rule, for hard fork core-343 + bool to_update_and_match_call_orders_for_hf_343 = false; if( (dgpo.next_maintenance_time <= HARDFORK_CORE_343_TIME) && (next_maintenance_time > HARDFORK_CORE_343_TIME) ) - to_update_and_match_call_orders = true; + to_update_and_match_call_orders_for_hf_343 = true; + + // Process inconsistent price feeds + if( (dgpo.next_maintenance_time <= HARDFORK_CORE_868_890_TIME) && (next_maintenance_time > HARDFORK_CORE_868_890_TIME) ) + process_hf_868_890( *this, to_update_and_match_call_orders_for_hf_343 ); + + // Explicitly call check_call_orders of all markets + if( (dgpo.next_maintenance_time <= HARDFORK_CORE_935_TIME) && (next_maintenance_time > HARDFORK_CORE_935_TIME) + && !to_update_and_match_call_orders_for_hf_343 ) + process_hf_935( *this ); + + // To reset call_price of all call orders, then match by new rule, for hard fork core-1270 + bool to_update_and_match_call_orders_for_hf_1270 = false; + if( (dgpo.next_maintenance_time <= HARDFORK_CORE_1270_TIME) && (next_maintenance_time > HARDFORK_CORE_1270_TIME) ) + to_update_and_match_call_orders_for_hf_1270 = true; modify(dgpo, [next_maintenance_time](dynamic_global_property_object& d) { d.next_maintenance_time = next_maintenance_time; d.accounts_registered_this_interval = 0; }); - // We need to do it after updated next_maintenance_time, to apply new rules here - if( to_update_and_match_call_orders ) - update_and_match_call_orders(*this); - - // Reset all BitAsset force settlement volumes to zero - for( const auto& d : get_index_type().indices() ) + // We need to do it after updated next_maintenance_time, to apply new rules here, for hard fork core-343 + if( to_update_and_match_call_orders_for_hf_343 ) { - modify( d, [](asset_bitasset_data_object& o) { o.force_settled_volume = 0; }); - if( d.has_settlement() ) - process_bids(d); + update_call_orders_hf_343(*this); + match_call_orders(*this); } + + // We need to do it after updated next_maintenance_time, to apply new rules here, for hard fork core-1270. + if( to_update_and_match_call_orders_for_hf_1270 ) + { + update_call_orders_hf_1270(*this); + update_median_feeds(*this); + match_call_orders(*this); + } + + process_bitassets(); + // Ideally we have to do this after every block but that leads to longer block applicaiton/replay times. // So keep it here as it is not critical. valid_to check ensures // these custom account auths and account roles are not usable. diff --git a/libraries/chain/db_market.cpp b/libraries/chain/db_market.cpp index 261b0dd5..45735652 100644 --- a/libraries/chain/db_market.cpp +++ b/libraries/chain/db_market.cpp @@ -43,12 +43,6 @@ namespace graphene { namespace chain { */ void database::globally_settle_asset( const asset_object& mia, const price& settlement_price ) { try { - /* - elog( "BLACK SWAN!" ); - debug_dump(); - edump( (mia.symbol)(settlement_price) ); - */ - const asset_bitasset_data_object& bitasset = mia.bitasset_data(*this); FC_ASSERT( !bitasset.has_settlement(), "black swan already occurred, it should not happen again" ); @@ -58,8 +52,7 @@ void database::globally_settle_asset( const asset_object& mia, const price& sett const asset_dynamic_data_object& mia_dyn = mia.dynamic_asset_data_id(*this); auto original_mia_supply = mia_dyn.current_supply; - const call_order_index& call_index = get_index_type(); - const auto& call_price_index = call_index.indices().get(); + const auto& call_price_index = get_index_type().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 @@ -90,9 +83,8 @@ void database::globally_settle_asset( const asset_object& mia, const price& sett FC_ASSERT( fill_call_order( order, pays, order.get_debt(), settlement_price, true ) ); // call order is maker } - modify( bitasset, [&]( asset_bitasset_data_object& obj ){ - assert( collateral_gathered.asset_id == settlement_price.quote.asset_id ); - obj.settlement_price = mia.amount(original_mia_supply) / collateral_gathered; //settlement_price; + modify( bitasset, [&mia,original_mia_supply,&collateral_gathered]( asset_bitasset_data_object& obj ){ + obj.settlement_price = mia.amount(original_mia_supply) / collateral_gathered; obj.settlement_fund = collateral_gathered.amount; }); @@ -100,7 +92,7 @@ void database::globally_settle_asset( const asset_object& mia, const price& sett /// that is a lie, the supply didn't change. We need to capture the current supply before /// filling all call orders and then restore it afterward. Then in the force settlement /// evaluator reduce the supply - modify( mia_dyn, [&]( asset_dynamic_data_object& obj ){ + modify( mia_dyn, [original_mia_supply]( asset_dynamic_data_object& obj ){ obj.current_supply = original_mia_supply; }); @@ -174,14 +166,20 @@ void database::execute_bid( const collateral_bid_object& bid, share_type debt_co call.borrower = bid.bidder; call.collateral = bid.inv_swan_price.base.amount + collateral_from_fund; call.debt = debt_covered; - call.call_price = price::call_price(asset(debt_covered, bid.inv_swan_price.quote.asset_id), - asset(call.collateral, bid.inv_swan_price.base.asset_id), - current_feed.maintenance_collateral_ratio); + // don't calculate call_price after core-1270 hard fork + if( get_dynamic_global_properties().next_maintenance_time > HARDFORK_CORE_1270_TIME ) + // bid.inv_swan_price is in collateral / debt + call.call_price = price( asset( 1, bid.inv_swan_price.base.asset_id ), + asset( 1, bid.inv_swan_price.quote.asset_id ) ); + else + call.call_price = price::call_price( asset(debt_covered, bid.inv_swan_price.quote.asset_id), + asset(call.collateral, bid.inv_swan_price.base.asset_id), + current_feed.maintenance_collateral_ratio ); }); if( bid.inv_swan_price.base.asset_id == asset_id_type() ) - modify(bid.bidder(*this).statistics(*this), [&](account_statistics_object& stats) { + modify(get_account_stats_by_owner(bid.bidder), [&](account_statistics_object& stats) { stats.total_core_in_orders += call_obj.collateral; }); @@ -429,6 +427,8 @@ bool database::apply_order(const limit_order_object& new_order_object, bool allo // 4. sell_asset has a valid price feed // 5. the call order's collateral ratio is below or equals to MCR // 6. the limit order provided a good price + auto maint_time = get_dynamic_global_properties().next_maintenance_time; + bool before_core_hardfork_1270 = ( maint_time <= HARDFORK_CORE_1270_TIME ); // call price caching issue bool to_check_call_orders = false; const asset_object& sell_asset = sell_asset_id( *this ); //const asset_object& recv_asset = recv_asset_id( *this ); @@ -442,7 +442,10 @@ bool database::apply_order(const limit_order_object& new_order_object, bool allo && !sell_abd->has_settlement() && !sell_abd->current_feed.settlement_price.is_null() ) { - call_match_price = ~sell_abd->current_feed.max_short_squeeze_price(); + if( before_core_hardfork_1270 ) + call_match_price = ~sell_abd->current_feed.max_short_squeeze_price_before_hf_1270(); + else + call_match_price = ~sell_abd->current_feed.max_short_squeeze_price(); if( ~new_order_object.sell_price <= call_match_price ) // new limit order price is good enough to match a call to_check_call_orders = true; } @@ -460,7 +463,33 @@ bool database::apply_order(const limit_order_object& new_order_object, bool allo finished = ( match( new_order_object, *old_limit_itr, old_limit_itr->sell_price ) != 2 ); } - if( !finished ) + if( !finished && !before_core_hardfork_1270 ) // TODO refactor or cleanup duplicate code after core-1270 hard fork + { + // check if there are margin calls + const auto& call_collateral_idx = get_index_type().indices().get(); + auto call_min = price::min( recv_asset_id, sell_asset_id ); + while( !finished ) + { + // hard fork core-343 and core-625 took place at same time, + // always check call order with least collateral ratio + auto call_itr = call_collateral_idx.lower_bound( call_min ); + if( call_itr == call_collateral_idx.end() + || call_itr->debt_type() != sell_asset_id + // feed protected https://github.com/cryptonomex/graphene/issues/436 + || call_itr->collateralization() > sell_abd->current_maintenance_collateralization ) + break; + // hard fork core-338 and core-625 took place at same time, not checking HARDFORK_CORE_338_TIME here. + 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, + sell_abd->current_maintenance_collateralization ); + // 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; + } + } + else if( !finished ) // and before core-1270 hard fork { // check if there are margin calls const auto& call_price_idx = get_index_type().indices().get(); @@ -477,7 +506,8 @@ bool database::apply_order(const limit_order_object& new_order_object, bool allo // assume hard fork core-338 and core-625 will take place at same time, not checking HARDFORK_CORE_338_TIME here. 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 ); + sell_abd->current_feed.maintenance_collateral_ratio, + optional() ); // 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 ) @@ -583,7 +613,8 @@ int database::match( const limit_order_object& usd, const limit_order_object& co } 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 ) + const price& feed_price, const uint16_t maintenance_collateral_ratio, + const optional& maintenance_collateralization ) { FC_ASSERT( bid.sell_asset_id() == ask.debt_type() ); FC_ASSERT( bid.receive_asset_id() == ask.collateral_type() ); @@ -607,7 +638,10 @@ int database::match( const limit_order_object& bid, const call_order_object& ask // 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 ), + asset( ask.get_max_debt_to_cover( match_price, + feed_price, + maintenance_collateral_ratio, + maintenance_collateralization ), ask.debt_type() ) ); asset call_pays, call_receives, order_pays, order_receives; @@ -817,9 +851,9 @@ bool database::fill_call_order( const call_order_object& order, const asset& pay const price& fill_price, const bool is_maker ) { try { //idump((pays)(receives)(order)); - 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 ); + FC_ASSERT( order.debt_type() == receives.asset_id ); + FC_ASSERT( order.collateral_type() == pays.asset_id ); + FC_ASSERT( order.collateral >= pays.amount ); const asset_object& mia = receives.asset_id(*this); FC_ASSERT( mia.is_market_issued() ); @@ -833,36 +867,39 @@ bool database::fill_call_order( const call_order_object& order, const asset& pay collateral_freed = o.get_collateral(); o.collateral = 0; } - else if( get_dynamic_global_properties().next_maintenance_time > HARDFORK_CORE_343_TIME ) - o.call_price = price::call_price( o.get_debt(), o.get_collateral(), - mia.bitasset_data(*this).current_feed.maintenance_collateral_ratio ); + else + { + auto maint_time = get_dynamic_global_properties().next_maintenance_time; + // update call_price after core-343 hard fork, + // but don't update call_price after core-1270 hard fork + if( maint_time <= HARDFORK_CORE_1270_TIME && maint_time > HARDFORK_CORE_343_TIME ) + { + o.call_price = price::call_price( o.get_debt(), o.get_collateral(), + mia.bitasset_data(*this).current_feed.maintenance_collateral_ratio ); + } + } }); const asset_dynamic_data_object& mia_ddo = mia.dynamic_asset_data_id(*this); - modify( mia_ddo, [&]( asset_dynamic_data_object& ao ){ + modify( mia_ddo, [&receives]( asset_dynamic_data_object& ao ){ //idump((receives)); ao.current_supply -= receives.amount; }); - const account_object& borrower = order.borrower(*this); - if( collateral_freed.valid() || pays.asset_id == asset_id_type() ) + // Adjust balance + if( collateral_freed.valid() ) + adjust_balance( order.borrower, *collateral_freed ); + // Update account statistics. We know that order.collateral_type() == pays.asset_id + if( pays.asset_id == asset_id_type() ) { - const account_statistics_object& borrower_statistics = borrower.statistics(*this); - if( collateral_freed.valid() ) - adjust_balance(borrower.get_id(), *collateral_freed); - - modify( borrower_statistics, [&]( account_statistics_object& b ){ - 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; - - assert( b.total_core_in_orders >= 0 ); - }); + modify( get_account_stats_by_owner(order.borrower), [&collateral_freed,&pays]( account_statistics_object& b ){ + b.total_core_in_orders -= pays.amount; + if( collateral_freed.valid() ) + b.total_core_in_orders -= collateral_freed->amount; + }); } - assert( pays.asset_id != receives.asset_id ); push_applied_operation( fill_order_operation( order.id, order.borrower, pays, receives, asset(0, pays.asset_id), fill_price, is_maker ) ); @@ -908,6 +945,8 @@ bool database::fill_settle_order( const force_settlement_object& settle, const a * @param mia - the market issued asset that should be called. * @param enable_black_swan - when adjusting collateral, triggering a black swan is invalid and will throw * if enable_black_swan is not set to true. + * @param for_new_limit_order - true if this function is called when matching call orders with a new limit order + * @param bitasset_ptr - an optional pointer to the bitasset_data object of the asset * * @return true if a margin call was executed. */ @@ -929,18 +968,17 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa if( bitasset.is_prediction_market ) return false; if( bitasset.current_feed.settlement_price.is_null() ) return false; - 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(); + bool before_core_hardfork_1270 = ( maint_time <= HARDFORK_CORE_1270_TIME ); // call price caching issue + // looking for limit orders selling the most USD for the least CORE auto max_price = price::max( mia.id, bitasset.options.short_backing_asset ); // stop when limit orders are selling too little USD for too much CORE - auto min_price = bitasset.current_feed.max_short_squeeze_price(); + auto min_price = ( before_core_hardfork_1270 ? bitasset.current_feed.max_short_squeeze_price_before_hf_1270() + : bitasset.current_feed.max_short_squeeze_price() ); - assert( max_price.base.asset_id == min_price.base.asset_id ); // NOTE limit_price_index is sorted from greatest to least auto limit_itr = limit_price_index.lower_bound( max_price ); auto limit_end = limit_price_index.upper_bound( min_price ); @@ -948,10 +986,28 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa if( limit_itr == limit_end ) return false; + const call_order_index& call_index = get_index_type(); + const auto& call_price_index = call_index.indices().get(); + const auto& call_collateral_index = call_index.indices().get(); + 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 call_price_itr = call_price_index.begin(); + auto call_price_end = call_price_itr; + auto call_collateral_itr = call_collateral_index.begin(); + auto call_collateral_end = call_collateral_itr; + + if( before_core_hardfork_1270 ) + { + call_price_itr = call_price_index.lower_bound( call_min ); + call_price_end = call_price_index.upper_bound( call_max ); + } + else + { + call_collateral_itr = call_collateral_index.lower_bound( call_min ); + call_collateral_end = call_collateral_index.upper_bound( call_max ); + } bool filled_limit = false; bool margin_called = false; @@ -969,34 +1025,32 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa 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 ) // TODO perhaps improve performance by passing in iterators + && limit_itr != limit_end + && ( ( !before_core_hardfork_1270 && call_collateral_itr != call_collateral_end ) + || ( before_core_hardfork_1270 && call_price_itr != call_price_end ) ) ) { bool filled_call = false; - price match_price; - asset usd_for_sale; - if( limit_itr != limit_end ) - { - assert( limit_itr != limit_price_index.end() ); - match_price = limit_itr->sell_price; - usd_for_sale = limit_itr->amount_for_sale(); - } - else return margin_called; - - match_price.validate(); - + const call_order_object& call_order = ( before_core_hardfork_1270 ? *call_price_itr : *call_collateral_itr ); // Feed protected (don't call if CR>MCR) https://github.com/cryptonomex/graphene/issues/436 - if( after_hardfork_436 && ( bitasset.current_feed.settlement_price > ~call_itr->call_price ) ) + if( ( !before_core_hardfork_1270 && bitasset.current_maintenance_collateralization < call_order.collateralization() ) + || ( before_core_hardfork_1270 + && after_hardfork_436 && bitasset.current_feed.settlement_price > ~call_order.call_price ) ) return margin_called; + const limit_order_object& limit_order = *limit_itr; + price match_price = limit_order.sell_price; + // There was a check `match_price.validate();` here, which is removed now because it always passes + // Old rule: margin calls can only buy high https://github.com/bitshares/bitshares-core/issues/606 - if( before_core_hardfork_606 && match_price > ~call_itr->call_price ) + if( before_core_hardfork_606 && match_price > ~call_order.call_price ) return margin_called; margin_called = true; - auto usd_to_buy = call_itr->get_debt(); + auto usd_to_buy = call_order.get_debt(); - if( usd_to_buy * match_price > call_itr->get_collateral() ) + if( usd_to_buy * match_price > call_order.get_collateral() ) { elog( "black swan detected on asset ${symbol} (${id}) at block ${b}", ("id",mia.id)("symbol",mia.symbol)("b",head_num) ); @@ -1006,11 +1060,21 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa return true; } - if( !before_core_hardfork_834 ) - usd_to_buy.amount = call_itr->get_max_debt_to_cover( match_price, + if( !before_core_hardfork_1270 ) + { + usd_to_buy.amount = call_order.get_max_debt_to_cover( match_price, bitasset.current_feed.settlement_price, - bitasset.current_feed.maintenance_collateral_ratio ); + bitasset.current_feed.maintenance_collateral_ratio, + bitasset.current_maintenance_collateralization ); + } + else if( !before_core_hardfork_834 ) + { + usd_to_buy.amount = call_order.get_max_debt_to_cover( match_price, + bitasset.current_feed.settlement_price, + bitasset.current_feed.maintenance_collateral_ratio ); + } + asset usd_for_sale = limit_order.amount_for_sale(); asset call_pays, call_receives, order_pays, order_receives; if( usd_to_buy > usd_for_sale ) { // fill order @@ -1071,17 +1135,18 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa call_pays = order_receives; order_pays = call_receives; - auto old_call_itr = call_itr; if( filled_call && before_core_hardfork_343 ) - ++call_itr; + ++call_price_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( !before_core_hardfork_343 ) - call_itr = call_price_index.lower_bound( call_min ); + fill_call_order(call_order, call_pays, call_receives, match_price, for_new_limit_order ); + if( !before_core_hardfork_1270 ) + call_collateral_itr = call_collateral_index.lower_bound( call_min ); + else if( !before_core_hardfork_343 ) + call_price_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 ); + bool really_filled = fill_limit_order( limit_order, order_pays, order_receives, true, match_price, !for_new_limit_order ); if( really_filled || ( filled_limit && before_core_hardfork_453 ) ) limit_itr = next_limit_itr; diff --git a/libraries/chain/db_update.cpp b/libraries/chain/db_update.cpp index bc8b3e57..3280040f 100644 --- a/libraries/chain/db_update.cpp +++ b/libraries/chain/db_update.cpp @@ -250,22 +250,40 @@ bool database::check_for_blackswan( const asset_object& mia, bool enable_black_s auto settle_price = bitasset.current_feed.settlement_price; if( settle_price.is_null() ) return false; // no feed - const call_order_index& call_index = get_index_type(); - const auto& call_price_index = call_index.indices().get(); + const call_order_object* call_ptr = nullptr; // place holder for the call order with least collateral ratio - 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 ); - - if( call_itr == call_end ) return false; // no call orders - - price highest = settle_price; + asset_id_type debt_asset_id = mia.id; + auto call_min = price::min( bitasset.options.short_backing_asset, debt_asset_id ); auto maint_time = get_dynamic_global_properties().next_maintenance_time; - if( maint_time > HARDFORK_CORE_338_TIME ) + bool before_core_hardfork_1270 = ( maint_time <= HARDFORK_CORE_1270_TIME ); // call price caching issue + + if( before_core_hardfork_1270 ) // before core-1270 hard fork, check with call_price + { + const auto& call_price_index = get_index_type().indices().get(); + auto call_itr = call_price_index.lower_bound( call_min ); + if( call_itr == call_price_index.end() ) // no call order + return false; + call_ptr = &(*call_itr); + } + else // after core-1270 hard fork, check with collateralization + { + const auto& call_collateral_index = get_index_type().indices().get(); + auto call_itr = call_collateral_index.lower_bound( call_min ); + if( call_itr == call_collateral_index.end() ) // no call order + return false; + call_ptr = &(*call_itr); + } + if( call_ptr->debt_type() != debt_asset_id ) // no call order + return false; + + price highest = settle_price; + if( maint_time > HARDFORK_CORE_1270_TIME ) // due to #338, we won't check for black swan on incoming limit order, so need to check with MSSP here highest = bitasset.current_feed.max_short_squeeze_price(); + else if( maint_time > HARDFORK_CORE_338_TIME ) + // due to #338, we won't check for black swan on incoming limit order, so need to check with MSSP here + highest = bitasset.current_feed.max_short_squeeze_price_before_hf_1270(); const limit_order_index& limit_index = get_index_type(); const auto& limit_price_index = limit_index.indices().get(); @@ -285,10 +303,10 @@ bool database::check_for_blackswan( const asset_object& mia, bool enable_black_s highest = std::max( limit_itr->sell_price, highest ); } - auto least_collateral = call_itr->collateralization(); + auto least_collateral = call_ptr->collateralization(); if( ~least_collateral >= highest ) { - wdump( (*call_itr) ); + wdump( (*call_ptr) ); elog( "Black Swan detected on asset ${symbol} (${id}) at block ${b}: \n" " Least collateralized call: ${lc} ${~lc}\n" // " Highest Bid: ${hb} ${~hb}\n" @@ -515,6 +533,7 @@ void database::clear_expired_orders() void database::update_expired_feeds() { const auto head_time = head_block_time(); + const auto next_maint_time = get_dynamic_global_properties().next_maintenance_time; bool after_hardfork_615 = ( head_time >= HARDFORK_615_TIME ); const auto& idx = get_index_type().indices().get(); @@ -529,9 +548,9 @@ void database::update_expired_feeds() if( after_hardfork_615 || b.feed_is_expired_before_hardfork_615( head_time ) ) { auto old_median_feed = b.current_feed; - modify( b, [head_time,&update_cer]( asset_bitasset_data_object& abdo ) + modify( b, [head_time,next_maint_time,&update_cer]( asset_bitasset_data_object& abdo ) { - abdo.update_median_feeds( head_time ); + abdo.update_median_feeds( head_time, next_maint_time ); if( abdo.need_to_update_cer() ) { update_cer = true; diff --git a/libraries/chain/hardfork.d/CORE_1270.hf b/libraries/chain/hardfork.d/CORE_1270.hf new file mode 100644 index 00000000..d32504cc --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_1270.hf @@ -0,0 +1,4 @@ +// bitshares-core issue #1270 Call price is inconsistent when MCR changed +#ifndef HARDFORK_CORE_1270_TIME +#define HARDFORK_CORE_1270_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/hardfork.d/CORE_518.hf b/libraries/chain/hardfork.d/CORE_518.hf new file mode 100644 index 00000000..b1c729d0 --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_518.hf @@ -0,0 +1,4 @@ +// bitshares-core issue #518 Clean up bitasset_data during maintenance +#ifndef HARDFORK_CORE_518_TIME +#define HARDFORK_CORE_518_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/hardfork.d/CORE_583.hf b/libraries/chain/hardfork.d/CORE_583.hf new file mode 100644 index 00000000..c2c602d5 --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_583.hf @@ -0,0 +1,4 @@ +// bitshares-core issue #583 Always allow updating a call order to higher collateral ratio +#ifndef HARDFORK_CORE_583_TIME +#define HARDFORK_CORE_583_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/hardfork.d/CORE_868_890.hf b/libraries/chain/hardfork.d/CORE_868_890.hf new file mode 100644 index 00000000..ca9e039d --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_868_890.hf @@ -0,0 +1,5 @@ +// bitshares-core issue #868 Clear price feed data after updated a bitAsset's backing asset ID +// bitshares-core issue #890 Update median feeds after feed_lifetime_sec changed +#ifndef HARDFORK_CORE_868_890_TIME +#define HARDFORK_CORE_868_890_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/hardfork.d/CORE_922_931.hf b/libraries/chain/hardfork.d/CORE_922_931.hf new file mode 100644 index 00000000..6a41d3dc --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_922_931.hf @@ -0,0 +1,5 @@ +// bitshares-core issue #922 Missing checks when updating an asset's bitasset_data +// bitshares-core issue #931 Changing backing asset ID runs some checks against the old value instead of the new +#ifndef HARDFORK_CORE_922_931_TIME +#define HARDFORK_CORE_922_931_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/hardfork.d/CORE_935.hf b/libraries/chain/hardfork.d/CORE_935.hf new file mode 100644 index 00000000..3449ac58 --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_935.hf @@ -0,0 +1,4 @@ +// bitshares-core issue #935 Call check_call_orders not only when settlement_price changed +#ifndef HARDFORK_CORE_935_TIME +#define HARDFORK_CORE_935_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/asset_evaluator.hpp b/libraries/chain/include/graphene/chain/asset_evaluator.hpp index d65d37fc..6e94a6c5 100644 --- a/libraries/chain/include/graphene/chain/asset_evaluator.hpp +++ b/libraries/chain/include/graphene/chain/asset_evaluator.hpp @@ -103,6 +103,7 @@ namespace graphene { namespace chain { void_result do_apply( const asset_update_bitasset_operation& o ); const asset_bitasset_data_object* bitasset_to_update = nullptr; + const asset_object* asset_to_update = nullptr; }; class asset_update_dividend_evaluator : public evaluator diff --git a/libraries/chain/include/graphene/chain/asset_object.hpp b/libraries/chain/include/graphene/chain/asset_object.hpp index d8c65e89..bcd195f5 100644 --- a/libraries/chain/include/graphene/chain/asset_object.hpp +++ b/libraries/chain/include/graphene/chain/asset_object.hpp @@ -209,6 +209,9 @@ namespace graphene { namespace chain { price_feed current_feed; /// This is the publication time of the oldest feed which was factored into current_feed. time_point_sec current_feed_publication_time; + /// Call orders with collateralization (aka collateral/debt) not greater than this value are in margin call territory. + /// This value is derived from @ref current_feed for better performance and should be kept consistent. + price current_maintenance_collateralization; /// True if this asset implements a @ref prediction_market bool is_prediction_market = false; @@ -260,7 +263,19 @@ namespace graphene { namespace chain { { return feed_expiration_time() >= current_time; } bool feed_is_expired(time_point_sec current_time)const { return feed_expiration_time() <= current_time; } - void update_median_feeds(time_point_sec current_time); + + /****** + * @brief calculate the median feed + * + * This calculates the median feed from @ref feeds, feed_lifetime_sec + * in @ref options, and the given parameters. + * It may update the current_feed_publication_time, current_feed and + * current_maintenance_collateralization member variables. + * + * @param current_time the current time to use in the calculations + * @param next_maintenance_time the next chain maintenance time + */ + void update_median_feeds(time_point_sec current_time, time_point_sec next_maintenance_time); }; // key extractor for short backing asset @@ -521,6 +536,7 @@ FC_REFLECT_DERIVED( graphene::chain::asset_bitasset_data_object, (graphene::db:: (feeds) (current_feed) (current_feed_publication_time) + (current_maintenance_collateralization) (options) (force_settled_volume) (is_prediction_market) diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index 70601df7..6b23d7dc 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -437,7 +437,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, - const price& feed_price, const uint16_t maintenance_collateral_ratio ); + const price& feed_price, const uint16_t maintenance_collateral_ratio, + const optional& maintenance_collateralization ); /// @return the amount of asset settled asset match(const call_order_object& call, const force_settlement_object& settle, @@ -584,6 +585,7 @@ namespace graphene { namespace chain { void update_son_wallet( const vector& new_active_sons ); void update_worker_votes(); void process_bids( const asset_bitasset_data_object& bad ); + void process_bitassets(); public: double calculate_vesting_factor(const account_object& stake_account); diff --git a/libraries/chain/include/graphene/chain/market_object.hpp b/libraries/chain/include/graphene/chain/market_object.hpp index 434e9e35..b01e25fa 100644 --- a/libraries/chain/include/graphene/chain/market_object.hpp +++ b/libraries/chain/include/graphene/chain/market_object.hpp @@ -133,8 +133,20 @@ class call_order_object : public abstract_object 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; + /** + * Calculate maximum quantity of debt to cover to satisfy @ref target_collateral_ratio. + * + * @param match_price the matching price if this call order is margin called + * @param feed_price median settlement price of debt asset + * @param maintenance_collateral_ratio median maintenance collateral ratio of debt asset + * @param maintenance_collateralization maintenance collateralization of debt asset, + * should only be valid after core-1270 hard fork + * @return maximum amount of debt that can be called + */ + share_type get_max_debt_to_cover( price match_price, + price feed_price, + const uint16_t maintenance_collateral_ratio, + const optional& maintenance_collateralization = optional() )const; }; /** diff --git a/libraries/chain/include/graphene/chain/protocol/asset.hpp b/libraries/chain/include/graphene/chain/protocol/asset.hpp index 8fe97bbc..54973d30 100644 --- a/libraries/chain/include/graphene/chain/protocol/asset.hpp +++ b/libraries/chain/include/graphene/chain/protocol/asset.hpp @@ -193,15 +193,6 @@ namespace graphene { namespace chain { /** Fixed point between 1.000 and 10.000, implied fixed point denominator is GRAPHENE_COLLATERAL_RATIO_DENOM */ uint16_t maximum_short_squeeze_ratio = GRAPHENE_DEFAULT_MAX_SHORT_SQUEEZE_RATIO; - /** - * When updating a call order the following condition must be maintained: - * - * debt * maintenance_price() < collateral - * debt * settlement_price < debt * maintenance - * debt * maintenance_price() < debt * max_short_squeeze_price() - price maintenance_price()const; - */ - /** When selling collateral to pay off debt, the least amount of debt to receive should be * min_usd = max_short_squeeze_price() * collateral * @@ -209,6 +200,13 @@ namespace graphene { namespace chain { * must be confirmed by having the max_short_squeeze_price() move below the black swan price. */ price max_short_squeeze_price()const; + /// Another implementation of max_short_squeeze_price() before the core-1270 hard fork + price max_short_squeeze_price_before_hf_1270()const; + + /// Call orders with collateralization (aka collateral/debt) not greater than this value are in margin call territory. + /// Calculation: ~settlement_price * maintenance_collateral_ratio / GRAPHENE_COLLATERAL_RATIO_DENOM + price maintenance_collateralization()const; + ///@} friend bool operator == ( const price_feed& a, const price_feed& b ) diff --git a/libraries/chain/market_evaluator.cpp b/libraries/chain/market_evaluator.cpp index 6d38285e..c7e3797b 100644 --- a/libraries/chain/market_evaluator.cpp +++ b/libraries/chain/market_evaluator.cpp @@ -227,11 +227,15 @@ void_result call_order_update_evaluator::do_apply(const call_order_update_operat } } + const auto next_maint_time = d.get_dynamic_global_properties().next_maintenance_time; + bool before_core_hardfork_1270 = ( next_maint_time <= HARDFORK_CORE_1270_TIME ); // call price caching issue auto& call_idx = d.get_index_type().indices().get(); auto itr = call_idx.find( boost::make_tuple(o.funding_account, o.delta_debt.asset_id) ); const call_order_object* call_obj = nullptr; + optional old_collateralization; + optional new_target_cr = o.extensions.value.target_collateral_ratio; if( itr == call_idx.end() ) @@ -239,19 +243,23 @@ void_result call_order_update_evaluator::do_apply(const call_order_update_operat FC_ASSERT( o.delta_collateral.amount > 0 ); FC_ASSERT( o.delta_debt.amount > 0 ); - call_obj = &d.create( [&](call_order_object& call ){ + call_obj = &d.create( [&o,this,before_core_hardfork_1270]( call_order_object& call ){ call.borrower = o.funding_account; call.collateral = o.delta_collateral.amount; 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; + if( before_core_hardfork_1270 ) // before core-1270 hard fork, calculate call_price here and cache it + call.call_price = price::call_price( o.delta_debt, o.delta_collateral, + _bitasset_data->current_feed.maintenance_collateral_ratio ); + else // after core-1270 hard fork, set call_price to 1 + call.call_price = price( asset( 1, o.delta_collateral.asset_id ), asset( 1, o.delta_debt.asset_id ) ); + call.target_collateral_ratio = o.extensions.value.target_collateral_ratio; }); } else { call_obj = &*itr; + old_collateralization = call_obj->collateralization(); d.modify( *call_obj, [&]( call_order_object& call ){ call.collateral += o.delta_collateral.amount; @@ -283,10 +291,16 @@ void_result call_order_update_evaluator::do_apply(const call_order_update_operat // check to see if the order needs to be margin called now, but don't allow black swans and require there to be // limit orders available that could be used to fill the order. + // Note: due to https://github.com/bitshares/bitshares-core/issues/649, + // the first call order may be unable to be updated if the second one is undercollateralized. if( d.check_call_orders( *_debt_asset, false ) ) { const auto call_obj = d.find(call_order_id); - // if we filled at least one call order, we are OK if we totally filled. + // before hard fork core-583: if we filled at least one call order, we are OK if we totally filled. + // after hard fork core-583: we want to allow increasing collateral + // Note: increasing collateral won't get the call order itself matched (instantly margin called) + // if there is at least a call order get matched but didn't cause a black swan event, + // current order must have got matched. in this case, it's OK if it's totally filled. GRAPHENE_ASSERT( !call_obj, call_order_update_unfilled_margin_call, @@ -298,16 +312,35 @@ void_result call_order_update_evaluator::do_apply(const call_order_update_operat { const auto call_obj = d.find(call_order_id); FC_ASSERT( call_obj, "no margin call was executed and yet the call object was deleted" ); - //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. - GRAPHENE_ASSERT( - ~call_obj->call_price < _bitasset_data->current_feed.settlement_price, - call_order_update_unfilled_margin_call, - "Updating call order would trigger a margin call that cannot be fully filled", - ("a", ~call_obj->call_price )("b", _bitasset_data->current_feed.settlement_price) - ); + if( d.head_block_time() <= HARDFORK_CORE_583_TIME ) // TODO remove after hard fork core-583 + { + // 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. + GRAPHENE_ASSERT( + ~call_obj->call_price < _bitasset_data->current_feed.settlement_price, + call_order_update_unfilled_margin_call, + "Updating call order would trigger a margin call that cannot be fully filled", + ("a", ~call_obj->call_price )("b", _bitasset_data->current_feed.settlement_price) + ); + } + else // after hard fork, always allow call order to be updated if collateral ratio is increased + { + // 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, + // if collateral ratio is not increased, we throw. + // be here, we know no margin call was executed, + // so call_obj's collateral ratio should be set only by op + FC_ASSERT( ( old_collateralization.valid() && call_obj->collateralization() > *old_collateralization ) + || ~call_obj->call_price < _bitasset_data->current_feed.settlement_price, + "Can only update to higher collateral ratio if it would trigger a margin call that cannot be fully filled", + ("new_call_price", ~call_obj->call_price ) + ("settlement_price", _bitasset_data->current_feed.settlement_price) + ("old_collateralization", old_collateralization) + ("new_collateralization", call_obj->collateralization() ) + ); + } } } @@ -367,6 +400,7 @@ void_result bid_collateral_evaluator::do_apply(const bid_collateral_operation& o bid.bidder = o.bidder; bid.inv_swan_price = o.additional_collateral / o.debt_covered; }); + // Note: CORE asset in collateral_bid_object is not counted in account_stats.total_core_in_orders return void_result(); } FC_CAPTURE_AND_RETHROW( (o) ) } diff --git a/libraries/chain/market_object.cpp b/libraries/chain/market_object.cpp index 9b66016a..c0107a63 100644 --- a/libraries/chain/market_object.cpp +++ b/libraries/chain/market_object.cpp @@ -25,6 +25,8 @@ #include +#include + using namespace graphene::chain; /* @@ -51,7 +53,8 @@ max_debt_to_cover = max_amount_to_sell * match_price */ share_type call_order_object::get_max_debt_to_cover( price match_price, price feed_price, - const uint16_t maintenance_collateral_ratio )const + const uint16_t maintenance_collateral_ratio, + const optional& maintenance_collateralization )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 ) @@ -60,7 +63,23 @@ share_type call_order_object::get_max_debt_to_cover( price match_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 + bool after_core_hardfork_1270 = maintenance_collateralization.valid(); + + // be defensive here, make sure maintenance_collateralization is in collateral / debt format + if( after_core_hardfork_1270 ) + { + FC_ASSERT( maintenance_collateralization->base.asset_id == call_price.base.asset_id + && maintenance_collateralization->quote.asset_id == call_price.quote.asset_id ); + } + + // According to the feed protection rule (https://github.com/cryptonomex/graphene/issues/436), + // a call order should only be called when its collateral ratio is not higher than required maintenance collateral ratio. + // Although this should be guaranteed by the caller of this function, we still check here to be defensive. + // Theoretically this check can be skipped for better performance. + // + // Before core-1270 hard fork, we check with call_price; afterwards, we check with collateralization(). + if( ( !after_core_hardfork_1270 && call_price > feed_price ) + || ( after_core_hardfork_1270 && collateralization() > *maintenance_collateralization ) ) return 0; if( !target_collateral_ratio.valid() ) // target cr is not set @@ -68,6 +87,10 @@ share_type call_order_object::get_max_debt_to_cover( price match_price, uint16_t tcr = std::max( *target_collateral_ratio, maintenance_collateral_ratio ); // use mcr if target cr is too small + price target_collateralization = ( after_core_hardfork_1270 ? + feed_price * ratio_type( tcr, GRAPHENE_COLLATERAL_RATIO_DENOM ) : + price() ); + // 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; @@ -108,9 +131,22 @@ share_type call_order_object::get_max_debt_to_cover( price match_price, 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 ) + // Check whether the collateral ratio after filled is high enough + // Before core-1270 hard fork, we check with call_price; afterwards, we check with collateralization(). + std::function result_is_good = after_core_hardfork_1270 ? + std::function( [this,&to_cover,&to_pay,target_collateralization]() -> bool + { + price new_collateralization = ( get_collateral() - to_pay ) / ( get_debt() - to_cover ); + return ( new_collateralization > target_collateralization ); + }) : + std::function( [this,&to_cover,&to_pay,tcr,feed_price]() -> bool + { + price new_call_price = price::call_price( get_debt() - to_cover, get_collateral() - to_pay, tcr ); + return ( new_call_price > feed_price ); + }); + + // if the result is good, we return. + if( result_is_good() ) return to_cover.amount; // be here, to_cover is too small due to rounding. deal with the fraction @@ -204,8 +240,8 @@ share_type call_order_object::get_max_debt_to_cover( price match_price, 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 + // Check whether the result is good + if( result_is_good() ) // good { if( to_pay.amount == max_to_pay.amount ) return to_cover.amount; @@ -253,8 +289,8 @@ share_type call_order_object::get_max_debt_to_cover( price match_price, // 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 + // Check whether the result is good + if( result_is_good() ) // good return to_cover.amount; } diff --git a/libraries/chain/protocol/asset.cpp b/libraries/chain/protocol/asset.cpp index 6906b338..b272aba5 100644 --- a/libraries/chain/protocol/asset.cpp +++ b/libraries/chain/protocol/asset.cpp @@ -227,7 +227,6 @@ namespace graphene { namespace chain { */ price price::call_price( const asset& debt, const asset& collateral, uint16_t collateral_ratio) { try { - //wdump((debt)(collateral)(collateral_ratio)); boost::rational swan(debt.amount.value,collateral.amount.value); boost::rational ratio( collateral_ratio, GRAPHENE_COLLATERAL_RATIO_DENOM ); auto cp = swan * ratio; @@ -259,9 +258,11 @@ namespace graphene { namespace chain { FC_ASSERT( maximum_short_squeeze_ratio <= GRAPHENE_MAX_COLLATERAL_RATIO ); FC_ASSERT( maintenance_collateral_ratio >= GRAPHENE_MIN_COLLATERAL_RATIO ); FC_ASSERT( maintenance_collateral_ratio <= GRAPHENE_MAX_COLLATERAL_RATIO ); - max_short_squeeze_price(); // make sure that it doesn't overflow + // Note: there was code here calling `max_short_squeeze_price();` before core-1270 hard fork, + // in order to make sure that it doesn't overflow, + // but the code doesn't actually check overflow, and it won't overflow, so the code is removed. - //FC_ASSERT( maintenance_collateral_ratio >= maximum_short_squeeze_ratio ); + // Note: not checking `maintenance_collateral_ratio >= maximum_short_squeeze_ratio` since launch } FC_CAPTURE_AND_RETHROW( (*this) ) } bool price_feed::is_for( asset_id_type asset_id ) const @@ -278,16 +279,34 @@ namespace graphene { namespace chain { FC_CAPTURE_AND_RETHROW( (*this) ) } - price price_feed::max_short_squeeze_price()const + // This function is kept here due to potential different behavior in edge cases. + // TODO check after core-1270 hard fork to see if we can safely remove it + price price_feed::max_short_squeeze_price_before_hf_1270()const { - boost::rational sp( settlement_price.base.amount.value, settlement_price.quote.amount.value ); //debt.amount.value,collateral.amount.value); + // settlement price is in debt/collateral + boost::rational sp( settlement_price.base.amount.value, settlement_price.quote.amount.value ); boost::rational ratio( GRAPHENE_COLLATERAL_RATIO_DENOM, maximum_short_squeeze_ratio ); auto cp = sp * ratio; while( cp.numerator() > GRAPHENE_MAX_SHARE_SUPPLY || cp.denominator() > GRAPHENE_MAX_SHARE_SUPPLY ) - cp = boost::rational( (cp.numerator() >> 1)+(cp.numerator()&1), (cp.denominator() >> 1)+(cp.denominator()&1) ); + cp = boost::rational( (cp.numerator() >> 1)+(cp.numerator()&1), + (cp.denominator() >> 1)+(cp.denominator()&1) ); - return (asset( cp.numerator().convert_to(), settlement_price.base.asset_id ) / asset( cp.denominator().convert_to(), settlement_price.quote.asset_id )); + return ( asset( cp.numerator().convert_to(), settlement_price.base.asset_id ) + / asset( cp.denominator().convert_to(), settlement_price.quote.asset_id ) ); + } + + price price_feed::max_short_squeeze_price()const + { + // settlement price is in debt/collateral + return settlement_price * ratio_type( GRAPHENE_COLLATERAL_RATIO_DENOM, maximum_short_squeeze_ratio ); + } + + price price_feed::maintenance_collateralization()const + { + if( settlement_price.is_null() ) + return price(); + return ~settlement_price * ratio_type( maintenance_collateral_ratio, GRAPHENE_COLLATERAL_RATIO_DENOM ); } // compile-time table of powers of 10 using template metaprogramming diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index 87492c7a..1fe92631 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -974,6 +974,38 @@ void database_fixture::publish_feed( const asset_object& mia, const account_obje verify_asset_supplies(db); } +/*** + * @brief helper method to add a price feed + * + * Adds a price feed for asset2, pushes the transaction, and generates the block + * + * @param fixture the database_fixture + * @param publisher who is publishing the feed + * @param asset1 the base asset + * @param amount1 the amount of the base asset + * @param asset2 the quote asset + * @param amount2 the amount of the quote asset + * @param core_id id of core (helps with core_exchange_rate) + */ +void database_fixture::publish_feed(const account_id_type& publisher, + const asset_id_type& asset1, int64_t amount1, + const asset_id_type& asset2, int64_t amount2, + const asset_id_type& core_id) +{ + const asset_object& a1 = asset1(db); + const asset_object& a2 = asset2(db); + const asset_object& core = core_id(db); + asset_publish_feed_operation op; + op.publisher = publisher; + op.asset_id = asset2; + op.feed.settlement_price = ~price(a1.amount(amount1),a2.amount(amount2)); + op.feed.core_exchange_rate = ~price(core.amount(amount1), a2.amount(amount2)); + trx.operations.push_back(std::move(op)); + PUSH_TX( db, trx, ~0); + generate_block(); + trx.clear(); +} + void database_fixture::force_global_settle( const asset_object& what, const price& p ) { try { set_expiration( db, trx ); diff --git a/tests/common/database_fixture.hpp b/tests/common/database_fixture.hpp index a7066154..4fa92ac9 100644 --- a/tests/common/database_fixture.hpp +++ b/tests/common/database_fixture.hpp @@ -228,6 +228,22 @@ struct database_fixture { void update_feed_producers(const asset_object& mia, flat_set producers); void publish_feed(asset_id_type mia, account_id_type by, const price_feed& f) { publish_feed(mia(db), by(db), f); } + /*** + * @brief helper method to add a price feed + * + * Adds a price feed for asset2, pushes the transaction, and generates the block + * + * @param publisher who is publishing the feed + * @param asset1 the base asset + * @param amount1 the amount of the base asset + * @param asset2 the quote asset + * @param amount2 the amount of the quote asset + * @param core_id id of core (helps with core_exchange_rate) + */ + void publish_feed(const account_id_type& publisher, + const asset_id_type& asset1, int64_t amount1, + const asset_id_type& asset2, int64_t amount2, + const asset_id_type& core_id); void publish_feed(const asset_object& mia, const account_object& by, const price_feed& f); const call_order_object* borrow(account_id_type who, asset what, asset collateral) { return borrow(who(db), what, collateral); }