diff --git a/libraries/app/api.cpp b/libraries/app/api.cpp index f36fd8d3..ab13ab12 100644 --- a/libraries/app/api.cpp +++ b/libraries/app/api.cpp @@ -539,6 +539,12 @@ namespace graphene { namespace app { break; case impl_global_betting_statistics_object_type: break; + case impl_collateral_bid_object_type:{ + const auto& aobj = dynamic_cast(obj); + assert( aobj != nullptr ); + accounts.insert( aobj->bidder ); + break; + } } } return result; diff --git a/libraries/chain/asset_evaluator.cpp b/libraries/chain/asset_evaluator.cpp index fb46efd5..5b7bc2a2 100644 --- a/libraries/chain/asset_evaluator.cpp +++ b/libraries/chain/asset_evaluator.cpp @@ -733,25 +733,49 @@ operation_result asset_settle_evaluator::do_apply(const asset_settle_evaluator:: const auto& bitasset = asset_to_settle->bitasset_data(d); if( bitasset.has_settlement() ) { - auto settled_amount = op.amount * bitasset.settlement_price; - FC_ASSERT( settled_amount.amount <= bitasset.settlement_fund ); - - d.modify( bitasset, [&]( asset_bitasset_data_object& obj ){ - obj.settlement_fund -= settled_amount.amount; - }); - - d.adjust_balance(op.account, settled_amount); - const auto& mia_dyn = asset_to_settle->dynamic_asset_data_id(d); + auto settled_amount = op.amount * bitasset.settlement_price; + if( op.amount.amount == mia_dyn.current_supply ) + settled_amount.amount = bitasset.settlement_fund; // avoid rounding problems + else + FC_ASSERT( settled_amount.amount < bitasset.settlement_fund ); + + if( settled_amount.amount == 0 && !bitasset.is_prediction_market ) + { + if( d.get_dynamic_global_properties().next_maintenance_time > HARDFORK_CORE_184_TIME ) + FC_THROW( "Settle amount is too small to receive anything due to rounding" ); + else // TODO remove this warning after hard fork core-184 + wlog( "Something for nothing issue (#184, variant F) occurred at block #${block}", ("block",d.head_block_num()) ); + } + + asset pays = op.amount; + if( op.amount.amount != mia_dyn.current_supply + && settled_amount.amount != 0 + && d.get_dynamic_global_properties().next_maintenance_time > HARDFORK_CORE_342_TIME ) + { + pays = settled_amount.multiply_and_round_up( bitasset.settlement_price ); + } + + d.adjust_balance( op.account, -pays ); + + if( settled_amount.amount > 0 ) + { + d.modify( bitasset, [&]( asset_bitasset_data_object& obj ){ + obj.settlement_fund -= settled_amount.amount; + }); + + d.adjust_balance( op.account, settled_amount ); + } d.modify( mia_dyn, [&]( asset_dynamic_data_object& obj ){ - obj.current_supply -= op.amount.amount; - }); + obj.current_supply -= pays.amount; + }); return settled_amount; } else { + d.adjust_balance( op.account, -op.amount ); return d.create([&](force_settlement_object& s) { s.owner = op.account; s.balance = op.amount; @@ -769,7 +793,10 @@ void_result asset_publish_feeds_evaluator::do_evaluate(const asset_publish_feed_ FC_ASSERT(base.is_market_issued()); const asset_bitasset_data_object& bitasset = base.bitasset_data(d); - FC_ASSERT( !bitasset.has_settlement(), "No further feeds may be published after a settlement event" ); + if( bitasset.is_prediction_market || d.head_block_time() <= HARDFORK_CORE_216_TIME ) + { + FC_ASSERT( !bitasset.has_settlement(), "No further feeds may be published after a settlement event" ); + } FC_ASSERT( o.feed.settlement_price.quote.asset_id == bitasset.options.short_backing_asset ); if( d.head_block_time() > HARDFORK_480_TIME ) @@ -820,7 +847,19 @@ void_result asset_publish_feeds_evaluator::do_apply(const asset_publish_feed_ope }); if( !(old_feed == bad.current_feed) ) + { + if( bad.has_settlement() ) // implies head_block_time > HARDFORK_CORE_216_TIME + { + 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 ) ) + d.revive_bitasset(base); + } db().check_call_orders(base); + } return void_result(); } FC_CAPTURE_AND_RETHROW((o)) } diff --git a/libraries/chain/db_debug.cpp b/libraries/chain/db_debug.cpp index 27beb3ed..60c33d80 100644 --- a/libraries/chain/db_debug.cpp +++ b/libraries/chain/db_debug.cpp @@ -43,6 +43,7 @@ void database::debug_dump() const auto& balance_index = db.get_index_type().indices(); const auto& statistics_index = db.get_index_type().indices(); + const auto& bids = db.get_index_type().indices(); map total_balances; map total_debts; share_type core_in_orders; @@ -58,6 +59,8 @@ void database::debug_dump() // idump(("statistics")(s)); reported_core_in_orders += s.total_core_in_orders; } + for( const collateral_bid_object& b : bids ) + total_balances[b.inv_swan_price.base.asset_id] += b.inv_swan_price.base.amount; for( const limit_order_object& o : db.get_index_type().indices() ) { // idump(("limit_order")(o)); diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index 724cad85..191024bf 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -327,6 +327,7 @@ void database::initialize_evaluators() register_evaluator(); register_evaluator(); register_evaluator(); + register_evaluator(); } void database::initialize_indexes() @@ -419,6 +420,7 @@ void database::initialize_indexes() add_index< primary_index >(); add_index< primary_index >(); add_index< primary_index >(); + add_index< primary_index >(); } diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index 848cb718..d923c1d0 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -1973,6 +1973,57 @@ void update_and_match_call_orders( database& db ) wlog( "Done updating all call orders for hardfork core-343 at block ${n}", ("n",db.head_block_num()) ); } +void database::process_bids( const asset_bitasset_data_object& bad ) +{ + if( bad.current_feed.settlement_price.is_null() ) return; + + asset_id_type to_revive_id = (asset( 0, bad.options.short_backing_asset ) * bad.settlement_price).asset_id; + const asset_object& to_revive = to_revive_id( *this ); + const asset_dynamic_data_object& bdd = to_revive.dynamic_data( *this ); + + const auto& bid_idx = get_index_type< collateral_bid_index >().indices().get(); + const auto start = bid_idx.lower_bound( boost::make_tuple( to_revive_id, price::max( bad.options.short_backing_asset, to_revive_id ), collateral_bid_id_type() ) ); + + share_type covered = 0; + auto itr = start; + while( itr != bid_idx.end() && itr->inv_swan_price.quote.asset_id == to_revive_id ) + { + const collateral_bid_object& bid = *itr; + asset total_collateral = bid.inv_swan_price.quote * bad.settlement_price; + total_collateral += bid.inv_swan_price.base; + price call_price = price::call_price( bid.inv_swan_price.quote, total_collateral, bad.current_feed.maintenance_collateral_ratio ); + if( ~call_price >= bad.current_feed.settlement_price ) break; + covered += bid.inv_swan_price.quote.amount; + ++itr; + if( covered >= bdd.current_supply ) break; + } + if( covered < bdd.current_supply ) return; + + const auto end = itr; + share_type to_cover = bdd.current_supply; + share_type remaining_fund = bad.settlement_fund; + for( itr = start; itr != end; ) + { + const collateral_bid_object& bid = *itr; + ++itr; + share_type debt = bid.inv_swan_price.quote.amount; + share_type collateral = (bid.inv_swan_price.quote * bad.settlement_price).amount; + if( bid.inv_swan_price.quote.amount >= to_cover ) + { + debt = to_cover; + collateral = remaining_fund; + } + to_cover -= debt; + remaining_fund -= collateral; + execute_bid( bid, debt, collateral, bad.current_feed ); + } + + FC_ASSERT( remaining_fund == 0 ); + FC_ASSERT( to_cover == 0 ); + + _cancel_bids_and_revive_mpa( to_revive, bad ); +} + void database::perform_chain_maintenance(const signed_block& next_block, const global_property_object& global_props) { try { const auto& gpo = get_global_properties(); @@ -2244,9 +2295,12 @@ void database::perform_chain_maintenance(const signed_block& next_block, const g update_and_match_call_orders(*this); // Reset all BitAsset force settlement volumes to zero - //for( const asset_bitasset_data_object* d : get_index_type() ) for( const auto& d : get_index_type().indices() ) + { modify( d, [](asset_bitasset_data_object& o) { o.force_settled_volume = 0; }); + if( d.has_settlement() ) + process_bids(d); + } // 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 80466a0c..1ab097f4 100644 --- a/libraries/chain/db_market.cpp +++ b/libraries/chain/db_market.cpp @@ -79,7 +79,7 @@ void database::globally_settle_asset( const asset_object& mia, const price& sett wlog( "Something for nothing issue (#184, variant E) occurred at block #${block}", ("block",head_block_num()) ); } else - pays = call_itr->get_debt() ^ settlement_price; // round up, in favor of global settlement fund + pays = call_itr->get_debt().multiply_and_round_up( settlement_price ); // round up, in favor of global settlement fund if( pays > call_itr->get_collateral() ) pays = call_itr->get_collateral(); @@ -106,6 +106,91 @@ void database::globally_settle_asset( const asset_object& mia, const price& sett } FC_CAPTURE_AND_RETHROW( (mia)(settlement_price) ) } +void database::revive_bitasset( const asset_object& bitasset ) +{ try { + FC_ASSERT( bitasset.is_market_issued() ); + const asset_bitasset_data_object& bad = bitasset.bitasset_data(*this); + FC_ASSERT( bad.has_settlement() ); + const asset_dynamic_data_object& bdd = bitasset.dynamic_asset_data_id(*this); + FC_ASSERT( !bad.is_prediction_market ); + FC_ASSERT( !bad.current_feed.settlement_price.is_null() ); + + if( bdd.current_supply > 0 ) + { + // Create + execute a "bid" with 0 additional collateral + const collateral_bid_object& pseudo_bid = create([&](collateral_bid_object& bid) { + bid.bidder = bitasset.issuer; + bid.inv_swan_price = asset(0, bad.options.short_backing_asset) + / asset(bdd.current_supply, bitasset.id); + }); + execute_bid( pseudo_bid, bdd.current_supply, bad.settlement_fund, bad.current_feed ); + } else + FC_ASSERT( bad.settlement_fund == 0 ); + + _cancel_bids_and_revive_mpa( bitasset, bad ); +} FC_CAPTURE_AND_RETHROW( (bitasset) ) } + +void database::_cancel_bids_and_revive_mpa( const asset_object& bitasset, const asset_bitasset_data_object& bad ) +{ try { + FC_ASSERT( bitasset.is_market_issued() ); + FC_ASSERT( bad.has_settlement() ); + FC_ASSERT( !bad.is_prediction_market ); + + // cancel remaining bids + const auto& bid_idx = get_index_type< collateral_bid_index >().indices().get(); + auto itr = bid_idx.lower_bound( boost::make_tuple( bitasset.id, price::max( bad.options.short_backing_asset, bitasset.id ), collateral_bid_id_type() ) ); + while( itr != bid_idx.end() && itr->inv_swan_price.quote.asset_id == bitasset.id ) + { + const collateral_bid_object& bid = *itr; + ++itr; + cancel_bid( bid ); + } + + // revive + modify( bad, [&]( asset_bitasset_data_object& obj ){ + obj.settlement_price = price(); + obj.settlement_fund = 0; + }); +} FC_CAPTURE_AND_RETHROW( (bitasset) ) } + +void database::cancel_bid(const collateral_bid_object& bid, bool create_virtual_op) +{ + adjust_balance(bid.bidder, bid.inv_swan_price.base); + + if( create_virtual_op ) + { + bid_collateral_operation vop; + vop.bidder = bid.bidder; + vop.additional_collateral = bid.inv_swan_price.base; + vop.debt_covered = asset( 0, bid.inv_swan_price.quote.asset_id ); + push_applied_operation( vop ); + } + remove(bid); +} + +void database::execute_bid( const collateral_bid_object& bid, share_type debt_covered, share_type collateral_from_fund, const price_feed& current_feed ) +{ + const call_order_object& call_obj = create( [&](call_order_object& call ){ + 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); + + }); + + if( bid.inv_swan_price.base.asset_id == asset_id_type() ) + modify(bid.bidder(*this).statistics(*this), [&](account_statistics_object& stats) { + stats.total_core_in_orders += call_obj.collateral; + }); + + push_applied_operation( execute_bid_operation( bid.bidder, asset( call_obj.collateral, bid.inv_swan_price.quote.asset_id ), + asset( debt_covered, bid.inv_swan_price.base.asset_id ) ) ); + + remove(bid); +} + void database::cancel_settle_order(const force_settlement_object& order, bool create_virtual_op) { adjust_balance(order.owner, order.balance); @@ -469,7 +554,7 @@ int database::match( const limit_order_object& usd, const limit_order_object& co // so we should cull the order in fill_limit_order() below. // The order would receive 0 even at `match_price`, so it would receive 0 at its own price, // so calling maybe_cull_small() will always cull it. - core_receives = usd_receives ^ match_price; + core_receives = usd_receives.multiply_and_round_up( match_price ); cull_taker = true; } } @@ -486,7 +571,7 @@ int database::match( const limit_order_object& usd, const limit_order_object& co else // The remaining amount in order `core` would be too small, // so the order will be culled in fill_limit_order() below - usd_receives = core_receives ^ match_price; + usd_receives = core_receives.multiply_and_round_up( match_price ); } core_pays = usd_receives; @@ -550,7 +635,7 @@ int database::match( const limit_order_object& bid, const call_order_object& ask // so we should cull the order in fill_limit_order() below. // The order would receive 0 even at `match_price`, so it would receive 0 at its own price, // so calling maybe_cull_small() will always cull it. - call_receives = order_receives ^ match_price; + call_receives = order_receives.multiply_and_round_up( match_price ); cull_taker = true; } } @@ -565,7 +650,7 @@ int database::match( const limit_order_object& bid, const call_order_object& ask return 1; } else // has hardfork core-342 - order_receives = usd_to_buy ^ match_price; // round up here, in favor of limit order + order_receives = usd_to_buy.multiply_and_round_up( match_price ); // round up here, in favor of limit order } call_pays = order_receives; @@ -629,7 +714,7 @@ asset database::match( const call_order_object& call, { if( call_receives == call_debt ) // the call order is smaller than or equal to the settle order { - call_pays = call_receives ^ match_price; // round up here, in favor of settle order + call_pays = call_receives.multiply_and_round_up( match_price ); // round up here, in favor of settle order // be here, we should have: call_pays <= call_collateral } else @@ -642,7 +727,7 @@ asset database::match( const call_order_object& call, cull_settle_order = true; // else do nothing, since we can't cull the settle order - call_receives = call_pays ^ match_price; // round up here to mitigate rouding issue (core-342) + call_receives = call_pays.multiply_and_round_up( match_price ); // round up here to mitigate rouding issue (core-342) if( call_receives == settle.balance ) // the settle order will be completely filled, no need to cull cull_settle_order = false; @@ -955,7 +1040,7 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa // so we should cull the order in fill_limit_order() below. // The order would receive 0 even at `match_price`, so it would receive 0 at its own price, // so calling maybe_cull_small() will always cull it. - call_receives = order_receives ^ match_price; + call_receives = order_receives.multiply_and_round_up( match_price ); filled_limit = true; } else { // fill call call_receives = usd_to_buy; @@ -968,7 +1053,7 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa wlog( "Something for nothing issue (#184, variant D) occurred at block #${block}", ("block",head_block_num()) ); } else - order_receives = usd_to_buy ^ match_price; // round up, in favor of limit order + order_receives = usd_to_buy.multiply_and_round_up( match_price ); // round up, in favor of limit order filled_call = true; // this is safe, since BSIP38 (hard fork core-834) depends on BSIP31 (hard fork core-343) if( usd_to_buy == usd_for_sale ) diff --git a/libraries/chain/db_notify.cpp b/libraries/chain/db_notify.cpp index c5986fad..b0ade40a 100644 --- a/libraries/chain/db_notify.cpp +++ b/libraries/chain/db_notify.cpp @@ -431,6 +431,12 @@ struct get_impacted_account_visitor void operator()( const random_number_store_operation& op ) { _impacted.insert( op.account ); } + void operator()( const bid_collateral_operation& op ) { + _impacted.insert( op.bidder ); + } + void operator()( const execute_bid_operation& op ) { + _impacted.insert( op.bidder ); + } }; void graphene::chain::operation_get_impacted_accounts( const operation& op, flat_set& result, bool ignore_custom_operation_required_auths ) { diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index dd85ec98..70601df7 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -409,6 +409,9 @@ namespace graphene { namespace chain { void globally_settle_asset( const asset_object& bitasset, const price& settle_price ); void cancel_settle_order(const force_settlement_object& order, bool create_virtual_op = true); void cancel_limit_order(const limit_order_object& order, bool create_virtual_op = true, bool skip_cancel_fee = false); + void revive_bitasset( const asset_object& bitasset ); + void cancel_bid(const collateral_bid_object& bid, bool create_virtual_op = true); + void execute_bid( const collateral_bid_object& bid, share_type debt_covered, share_type collateral_from_fund, const price_feed& current_feed ); /** * @brief Process a new limit order through the markets @@ -527,6 +530,7 @@ namespace graphene { namespace chain { private: void _apply_block( const signed_block& next_block ); processed_transaction _apply_transaction( const signed_transaction& trx ); + void _cancel_bids_and_revive_mpa( const asset_object& bitasset, const asset_bitasset_data_object& bad ); ///Steps involved in applying a new block ///@{ @@ -579,6 +583,7 @@ namespace graphene { namespace chain { void update_son_statuses( const vector& cur_active_sons, const vector& new_active_sons ); void update_son_wallet( const vector& new_active_sons ); void update_worker_votes(); + void process_bids( const asset_bitasset_data_object& bad ); public: double calculate_vesting_factor(const account_object& stake_account); diff --git a/libraries/chain/include/graphene/chain/market_evaluator.hpp b/libraries/chain/include/graphene/chain/market_evaluator.hpp index b18f6a43..a5ba0981 100644 --- a/libraries/chain/include/graphene/chain/market_evaluator.hpp +++ b/libraries/chain/include/graphene/chain/market_evaluator.hpp @@ -31,6 +31,7 @@ namespace graphene { namespace chain { class asset_object; class asset_bitasset_data_object; class call_order_object; + struct bid_collateral_operation; struct call_order_update_operation; struct limit_order_cancel_operation; struct limit_order_create_operation; @@ -88,4 +89,18 @@ namespace graphene { namespace chain { const asset_bitasset_data_object* _bitasset_data = nullptr; }; + class bid_collateral_evaluator : public evaluator + { + public: + typedef bid_collateral_operation operation_type; + + void_result do_evaluate( const bid_collateral_operation& o ); + void_result do_apply( const bid_collateral_operation& o ); + + const asset_object* _debt_asset = nullptr; + const asset_bitasset_data_object* _bitasset_data = nullptr; + const account_object* _paying_account = nullptr; + const collateral_bid_object* _bid = nullptr; + }; + } } // graphene::chain diff --git a/libraries/chain/include/graphene/chain/market_object.hpp b/libraries/chain/include/graphene/chain/market_object.hpp index b87a7af5..27a31fe7 100644 --- a/libraries/chain/include/graphene/chain/market_object.hpp +++ b/libraries/chain/include/graphene/chain/market_object.hpp @@ -157,6 +157,27 @@ class force_settlement_object : public abstract_object { return balance.asset_id; } }; +/** + * @class collateral_bid_object + * @brief bids of collateral for debt after a black swan + * + * There should only be one collateral_bid_object per asset per account, and + * only for smartcoin assets that have a global settlement_price. + */ +class collateral_bid_object : public abstract_object +{ + public: + static const uint8_t space_id = implementation_ids; + static const uint8_t type_id = impl_collateral_bid_object_type; + + asset get_additional_collateral()const { return inv_swan_price.base; } + asset get_debt_covered()const { return inv_swan_price.quote; } + asset_id_type debt_type()const { return inv_swan_price.quote.asset_id; } + + account_id_type bidder; + price inv_swan_price; // Collateral / Debt +}; + struct by_collateral; struct by_account; struct by_price; @@ -208,8 +229,31 @@ typedef multi_index_container< > > force_settlement_object_multi_index_type; +typedef multi_index_container< + collateral_bid_object, + indexed_by< + ordered_unique< tag, + member< object, object_id_type, &object::id > >, + ordered_unique< tag, + composite_key< collateral_bid_object, + const_mem_fun< collateral_bid_object, asset_id_type, &collateral_bid_object::debt_type>, + member< collateral_bid_object, account_id_type, &collateral_bid_object::bidder> + > + >, + ordered_unique< tag, + composite_key< collateral_bid_object, + const_mem_fun< collateral_bid_object, asset_id_type, &collateral_bid_object::debt_type>, + member< collateral_bid_object, price, &collateral_bid_object::inv_swan_price >, + member< object, object_id_type, &object::id > + >, + composite_key_compare< std::less, std::greater, std::less > + > + > +> collateral_bid_object_multi_index_type; + typedef generic_index call_order_index; typedef generic_index force_settlement_index; +typedef generic_index collateral_bid_index; } } // graphene::chain @@ -225,7 +269,10 @@ FC_REFLECT_DERIVED( graphene::chain::force_settlement_object, (graphene::db::object), (owner)(balance)(settlement_date) ) +FC_REFLECT_DERIVED( graphene::chain::collateral_bid_object, (graphene::db::object), + (bidder)(inv_swan_price) ) GRAPHENE_EXTERNAL_SERIALIZATION( extern, graphene::chain::limit_order_object ) GRAPHENE_EXTERNAL_SERIALIZATION( extern, graphene::chain::call_order_object ) GRAPHENE_EXTERNAL_SERIALIZATION( extern, graphene::chain::force_settlement_object ) +GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::chain::collateral_bid_object ) \ No newline at end of file diff --git a/libraries/chain/include/graphene/chain/protocol/asset.hpp b/libraries/chain/include/graphene/chain/protocol/asset.hpp index 60bd3cd0..3a35fea0 100644 --- a/libraries/chain/include/graphene/chain/protocol/asset.hpp +++ b/libraries/chain/include/graphene/chain/protocol/asset.hpp @@ -29,6 +29,8 @@ namespace graphene { namespace chain { extern const int64_t scaled_precision_lut[]; + struct price; + struct asset { asset( share_type a = 0, asset_id_type id = asset_id_type() ) @@ -94,6 +96,8 @@ namespace graphene { namespace chain { FC_ASSERT( precision < 19 ); return scaled_precision_lut[ precision ]; } + + asset multiply_and_round_up( const price& p )const; ///< Multiply and round up }; /** diff --git a/libraries/chain/include/graphene/chain/protocol/market.hpp b/libraries/chain/include/graphene/chain/protocol/market.hpp index e9622e27..50a8873b 100644 --- a/libraries/chain/include/graphene/chain/protocol/market.hpp +++ b/libraries/chain/include/graphene/chain/protocol/market.hpp @@ -172,17 +172,69 @@ namespace graphene { namespace chain { share_type calculate_fee(const fee_parameters_type& k)const { return 0; } }; + /** + * @ingroup operations + * + * This operation can be used after a black swan to bid collateral for + * taking over part of the debt and the settlement_fund (see BSIP-0018). + */ + struct bid_collateral_operation : public base_operation + { + /** should be equivalent to call_order_update fee */ + struct fee_parameters_type { uint64_t fee = 20 * GRAPHENE_BLOCKCHAIN_PRECISION; }; + + asset fee; + account_id_type bidder; ///< pays fee and additional collateral + asset additional_collateral; ///< the amount of collateral to bid for the debt + asset debt_covered; ///< the amount of debt to take over + extensions_type extensions; + + account_id_type fee_payer()const { return bidder; } + void validate()const; + }; + + /** + * @ingroup operations + * + * @note This is a virtual operation that is created while reviving a + * bitasset from collateral bids. + */ + struct execute_bid_operation : public base_operation + { + struct fee_parameters_type {}; + + execute_bid_operation(){} + execute_bid_operation( account_id_type a, asset d, asset c ) + : bidder(a), debt(d), collateral(c) {} + + asset fee; + account_id_type bidder; + asset debt; + asset collateral; + + account_id_type fee_payer()const { return bidder; } + void validate()const { FC_ASSERT( !"virtual operation" ); } + + /// This is a virtual operation; there is no fee + share_type calculate_fee(const fee_parameters_type& k)const { return 0; } + }; + } } // graphene::chain FC_REFLECT( graphene::chain::limit_order_create_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::limit_order_cancel_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::call_order_update_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::bid_collateral_operation::fee_parameters_type, (fee) ) /// THIS IS THE ONLY VIRTUAL OPERATION THUS FAR... FC_REFLECT( graphene::chain::fill_order_operation::fee_parameters_type, ) +FC_REFLECT( graphene::chain::fill_order_operation::fee_parameters_type, ) // VIRTUAL +FC_REFLECT( graphene::chain::execute_bid_operation::fee_parameters_type, ) // VIRTUAL FC_REFLECT( graphene::chain::limit_order_create_operation,(fee)(seller)(amount_to_sell)(min_to_receive)(expiration)(fill_or_kill)(extensions)) FC_REFLECT( graphene::chain::limit_order_cancel_operation,(fee)(fee_paying_account)(order)(extensions) ) FC_REFLECT( graphene::chain::call_order_update_operation, (fee)(funding_account)(delta_collateral)(delta_debt)(extensions) ) FC_REFLECT( graphene::chain::fill_order_operation, (fee)(order_id)(account_id)(pays)(receives)(fill_price)(is_maker) ) +FC_REFLECT( graphene::chain::bid_collateral_operation, (fee)(bidder)(additional_collateral)(debt_covered)(extensions) ) +FC_REFLECT( graphene::chain::execute_bid_operation, (fee)(bidder)(debt)(collateral) ) FC_REFLECT( graphene::chain::call_order_update_operation::options_type, (target_collateral_ratio) ) @@ -194,3 +246,5 @@ GRAPHENE_EXTERNAL_SERIALIZATION( extern, graphene::chain::limit_order_create_ope GRAPHENE_EXTERNAL_SERIALIZATION( extern, graphene::chain::limit_order_cancel_operation ) GRAPHENE_EXTERNAL_SERIALIZATION( extern, graphene::chain::call_order_update_operation ) GRAPHENE_EXTERNAL_SERIALIZATION( extern, graphene::chain::fill_order_operation ) +GRAPHENE_EXTERNAL_SERIALIZATION( extern, graphene::chain::bid_collateral_operation ) +GRAPHENE_EXTERNAL_SERIALIZATION( extern, graphene::chain::execute_bid_operation ) diff --git a/libraries/chain/include/graphene/chain/protocol/operations.hpp b/libraries/chain/include/graphene/chain/protocol/operations.hpp index 83d347ab..c704cb94 100644 --- a/libraries/chain/include/graphene/chain/protocol/operations.hpp +++ b/libraries/chain/include/graphene/chain/protocol/operations.hpp @@ -190,7 +190,9 @@ namespace graphene { namespace chain { nft_lottery_token_purchase_operation, nft_lottery_reward_operation, nft_lottery_end_operation, - random_number_store_operation + random_number_store_operation, + bid_collateral_operation, + execute_bid_operation > operation; /// @} // operations group diff --git a/libraries/chain/include/graphene/chain/protocol/types.hpp b/libraries/chain/include/graphene/chain/protocol/types.hpp index 321b08d9..bb659586 100644 --- a/libraries/chain/include/graphene/chain/protocol/types.hpp +++ b/libraries/chain/include/graphene/chain/protocol/types.hpp @@ -216,7 +216,8 @@ namespace graphene { namespace chain { impl_offer_history_object_type, impl_son_statistics_object_type, impl_son_schedule_object_type, - impl_nft_lottery_balance_object_type + impl_nft_lottery_balance_object_type, + impl_collateral_bid_object_type }; //typedef fc::unsigned_int object_id_type; @@ -328,6 +329,7 @@ namespace graphene { namespace chain { class nft_lottery_balance_object; class son_statistics_object; class son_schedule_object; + class collateral_bid_object; typedef object_id< implementation_ids, impl_global_property_object_type, global_property_object> global_property_id_type; typedef object_id< implementation_ids, impl_dynamic_global_property_object_type, dynamic_global_property_object> dynamic_global_property_id_type; @@ -360,6 +362,7 @@ namespace graphene { namespace chain { typedef object_id< implementation_ids, impl_nft_lottery_balance_object_type, nft_lottery_balance_object> nft_lottery_balance_id_type; typedef object_id< implementation_ids, impl_son_statistics_object_type, son_statistics_object > son_statistics_id_type; typedef object_id< implementation_ids, impl_son_schedule_object_type, son_schedule_object> son_schedule_id_type; + typedef object_id< implementation_ids, impl_collateral_bid_object_type, collateral_bid_object > collateral_bid_id_type; typedef fc::array symbol_type; typedef fc::ripemd160 block_id_type; @@ -534,6 +537,7 @@ FC_REFLECT_ENUM( graphene::chain::impl_object_type, (impl_son_statistics_object_type) (impl_son_schedule_object_type) (impl_nft_lottery_balance_object_type) + (impl_collateral_bid_object_type) ) FC_REFLECT_TYPENAME( graphene::chain::share_type ) @@ -592,6 +596,7 @@ FC_REFLECT_TYPENAME( graphene::chain::son_wallet_withdraw_id_type ) FC_REFLECT_TYPENAME( graphene::chain::sidechain_address_id_type ) FC_REFLECT_TYPENAME( graphene::chain::sidechain_transaction_id_type ) FC_REFLECT_TYPENAME( graphene::chain::random_number_id_type ) +FC_REFLECT_TYPENAME( graphene::chain::collateral_bid_id_type ) FC_REFLECT( graphene::chain::void_t, ) diff --git a/libraries/chain/market_evaluator.cpp b/libraries/chain/market_evaluator.cpp index a6a21165..6d38285e 100644 --- a/libraries/chain/market_evaluator.cpp +++ b/libraries/chain/market_evaluator.cpp @@ -314,4 +314,61 @@ void_result call_order_update_evaluator::do_apply(const call_order_update_operat return void_result(); } FC_CAPTURE_AND_RETHROW( (o) ) } +void_result bid_collateral_evaluator::do_evaluate(const bid_collateral_operation& o) +{ try { + database& d = db(); + + FC_ASSERT( d.head_block_time() >= HARDFORK_CORE_216_TIME, "Not yet!" ); + + _paying_account = &o.bidder(d); + _debt_asset = &o.debt_covered.asset_id(d); + FC_ASSERT( _debt_asset->is_market_issued(), "Unable to cover ${sym} as it is not a collateralized asset.", + ("sym", _debt_asset->symbol) ); + + _bitasset_data = &_debt_asset->bitasset_data(d); + + FC_ASSERT( _bitasset_data->has_settlement() ); + + FC_ASSERT( o.additional_collateral.asset_id == _bitasset_data->options.short_backing_asset ); + + FC_ASSERT( !_bitasset_data->is_prediction_market, "Cannot bid on a prediction market!" ); + + if( o.additional_collateral.amount > 0 ) + { + FC_ASSERT( d.get_balance(*_paying_account, _bitasset_data->options.short_backing_asset(d)) >= o.additional_collateral, + "Cannot bid ${c} collateral when payer only has ${b}", ("c", o.additional_collateral.amount) + ("b", d.get_balance(*_paying_account, o.additional_collateral.asset_id(d)).amount) ); + } + + const collateral_bid_index& bids = d.get_index_type(); + const auto& index = bids.indices().get(); + const auto& bid = index.find( boost::make_tuple( o.debt_covered.asset_id, o.bidder ) ); + if( bid != index.end() ) + _bid = &(*bid); + else + FC_ASSERT( o.debt_covered.amount > 0, "Can't find bid to cancel?!"); + + return void_result(); +} FC_CAPTURE_AND_RETHROW( (o) ) } + + +void_result bid_collateral_evaluator::do_apply(const bid_collateral_operation& o) +{ try { + database& d = db(); + + if( _bid ) + d.cancel_bid( *_bid, false ); + + if( o.debt_covered.amount == 0 ) return void_result(); + + d.adjust_balance( o.bidder, -o.additional_collateral ); + + _bid = &d.create([&]( collateral_bid_object& bid ) { + bid.bidder = o.bidder; + bid.inv_swan_price = o.additional_collateral / o.debt_covered; + }); + + return void_result(); +} FC_CAPTURE_AND_RETHROW( (o) ) } + } } // graphene::chain diff --git a/libraries/chain/protocol/asset.cpp b/libraries/chain/protocol/asset.cpp index 525e193b..4a7cb51c 100644 --- a/libraries/chain/protocol/asset.cpp +++ b/libraries/chain/protocol/asset.cpp @@ -93,6 +93,18 @@ namespace graphene { namespace chain { FC_THROW_EXCEPTION( fc::assert_exception, "invalid asset * price", ("asset",a)("price",b) ); } + asset asset::multiply_and_round_up( const price& b )const + { + const asset& a = *this; + if( a.asset_id == b.base.asset_id ) + { + FC_ASSERT( b.base.amount.value > 0 ); + FC_ASSERT( result <= GRAPHENE_MAX_SHARE_SUPPLY ); + return asset( result.convert_to(), b.base.asset_id ); + } + FC_THROW_EXCEPTION( fc::assert_exception, "invalid asset::multiply_and_round_up(price)", ("asset",a)("price",b) ); + } + price operator / ( const asset& base, const asset& quote ) { try { FC_ASSERT( base.asset_id != quote.asset_id ); diff --git a/libraries/chain/protocol/market.cpp b/libraries/chain/protocol/market.cpp index ae0a3a68..0079e22c 100644 --- a/libraries/chain/protocol/market.cpp +++ b/libraries/chain/protocol/market.cpp @@ -46,6 +46,12 @@ void call_order_update_operation::validate()const FC_ASSERT( delta_collateral.amount != 0 || delta_debt.amount != 0 ); } FC_CAPTURE_AND_RETHROW((*this)) } +void bid_collateral_operation::validate()const +{ try { + FC_ASSERT( fee.amount >= 0 ); + FC_ASSERT( debt_covered.amount == 0 || (debt_covered.amount > 0 && additional_collateral.amount > 0) ); +} FC_CAPTURE_AND_RETHROW((*this)) } + } } // graphene::chain GRAPHENE_EXTERNAL_SERIALIZATION( /*not extern*/, graphene::chain::limit_order_create_operation::fee_parameters_type ) diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index 47989006..87492c7a 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -254,6 +254,7 @@ void database_fixture::verify_asset_supplies( const database& db ) const auto& settle_index = db.get_index_type().indices(); const auto& tournaments_index = db.get_index_type().indices(); const auto& asst_index = db.get_index_type().indices(); + const auto& bids = db.get_index_type().indices(); map total_balances; map total_debts; @@ -273,6 +274,8 @@ void database_fixture::verify_asset_supplies( const database& db ) total_balances[b.asset_type] += b.balance; for( const force_settlement_object& s : settle_index ) total_balances[s.balance.asset_id] += s.balance.amount; + for( const collateral_bid_object& b : bids ) + total_balances[b.inv_swan_price.base.asset_id] += b.inv_swan_price.base.amount; for( const account_statistics_object& a : statistics_index ) { reported_core_in_orders += a.total_core_in_orders; @@ -1044,6 +1047,22 @@ void database_fixture::cover(const account_object& who, asset what, asset collat verify_asset_supplies(db); } FC_CAPTURE_AND_RETHROW( (who.name)(what)(collateral) ) } +void database_fixture::bid_collateral(const account_object& who, const asset& to_bid, const asset& to_cover) +{ try { + set_expiration( db, trx ); + trx.operations.clear(); + bid_collateral_operation bid; + bid.bidder = who.id; + bid.additional_collateral = to_bid; + bid.debt_covered = to_cover; + trx.operations.push_back(bid); + for( auto& op : trx.operations ) db.current_fee_schedule().set_fee(op); + trx.validate(); + db.push_transaction(trx, ~0); + trx.operations.clear(); + verify_asset_supplies(db); +} FC_CAPTURE_AND_RETHROW( (who.name)(to_bid)(to_cover) ) } + void database_fixture::fund_fee_pool( const account_object& from, const asset_object& asset_to_fund, const share_type amount ) { asset_fund_fee_pool_operation fund; diff --git a/tests/common/database_fixture.hpp b/tests/common/database_fixture.hpp index b3a7d5b7..a7066154 100644 --- a/tests/common/database_fixture.hpp +++ b/tests/common/database_fixture.hpp @@ -235,6 +235,7 @@ struct database_fixture { void cover(account_id_type who, asset what, asset collateral_freed) { cover(who(db), what, collateral_freed); } void cover(const account_object& who, asset what, asset collateral_freed); + void bid_collateral(const account_object& who, const asset& to_bid, const asset& to_cover); const asset_object& get_asset( const string& symbol )const; const account_object& get_account( const string& name )const;