diff --git a/libraries/chain/CMakeLists.txt b/libraries/chain/CMakeLists.txt index 8443841eef..6fe3f47883 100644 --- a/libraries/chain/CMakeLists.txt +++ b/libraries/chain/CMakeLists.txt @@ -84,6 +84,7 @@ add_library( graphene_chain account_object.cpp asset_object.cpp fba_object.cpp + market_object.cpp proposal_object.cpp vesting_balance_object.cpp diff --git a/libraries/chain/db_market.cpp b/libraries/chain/db_market.cpp index 5ee9d57abc..f1fb8cf77d 100644 --- a/libraries/chain/db_market.cpp +++ b/libraries/chain/db_market.cpp @@ -464,16 +464,23 @@ bool database::apply_order(const limit_order_object& new_order_object, bool allo // check if there are margin calls const auto& call_price_idx = get_index_type().indices().get(); auto call_min = price::min( recv_asset_id, sell_asset_id ); - auto call_itr = call_price_idx.lower_bound( call_min ); - // feed protected https://github.com/cryptonomex/graphene/issues/436 - auto call_end = call_price_idx.upper_bound( ~sell_abd->current_feed.settlement_price ); - while( !finished && call_itr != call_end ) + while( !finished ) { - auto old_call_itr = call_itr; - ++call_itr; // would be safe, since we'll end the loop if a call order is partially matched - // match returns 2 when only the old order was fully filled. In this case, we keep matching; otherwise, we stop. + // assume hard fork core-343 and core-625 will take place at same time, always check call order with least call_price + auto call_itr = call_price_idx.lower_bound( call_min ); + if( call_itr == call_price_idx.end() + || call_itr->debt_type() != sell_asset_id + // feed protected https://github.com/cryptonomex/graphene/issues/436 + || call_itr->call_price > ~sell_abd->current_feed.settlement_price ) + break; // assume hard fork core-338 and core-625 will take place at same time, not checking HARDFORK_CORE_338_TIME here. - finished = ( match( new_order_object, *old_call_itr, call_match_price ) != 2 ); + int match_result = match( new_order_object, *call_itr, call_match_price, + sell_abd->current_feed.settlement_price, + sell_abd->current_feed.maintenance_collateral_ratio ); + // match returns 1 or 3 when the new order was fully filled. In this case, we stop matching; otherwise keep matching. + // since match can return 0 due to BSIP38 (hard fork core-834), we no longer only check if the result is 2. + if( match_result == 1 || match_result == 3 ) + finished = true; } } } @@ -574,7 +581,8 @@ int database::match( const limit_order_object& usd, const limit_order_object& co return result; } -int database::match( const limit_order_object& bid, const call_order_object& ask, const price& match_price ) +int database::match( const limit_order_object& bid, const call_order_object& ask, const price& match_price, + const price& feed_price, const uint16_t maintenance_collateral_ratio ) { FC_ASSERT( bid.sell_asset_id() == ask.debt_type() ); FC_ASSERT( bid.receive_asset_id() == ask.collateral_type() ); @@ -583,16 +591,25 @@ int database::match( const limit_order_object& bid, const call_order_object& ask auto maint_time = get_dynamic_global_properties().next_maintenance_time; // TODO remove when we're sure it's always false bool before_core_hardfork_184 = ( maint_time <= HARDFORK_CORE_184_TIME ); // something-for-nothing + // TODO remove when we're sure it's always false bool before_core_hardfork_342 = ( maint_time <= HARDFORK_CORE_342_TIME ); // better rounding + // TODO remove when we're sure it's always false + bool before_core_hardfork_834 = ( maint_time <= HARDFORK_CORE_834_TIME ); // target collateral ratio option if( before_core_hardfork_184 ) ilog( "match(limit,call) is called before hardfork core-184 at block #${block}", ("block",head_block_num()) ); if( before_core_hardfork_342 ) ilog( "match(limit,call) is called before hardfork core-342 at block #${block}", ("block",head_block_num()) ); + if( before_core_hardfork_834 ) + ilog( "match(limit,call) is called before hardfork core-834 at block #${block}", ("block",head_block_num()) ); bool cull_taker = false; asset usd_for_sale = bid.amount_for_sale(); - asset usd_to_buy = ask.get_debt(); + // TODO if we're sure `before_core_hardfork_834` is always false, remove the check + asset usd_to_buy = ( before_core_hardfork_834 ? + ask.get_debt() : + asset( ask.get_max_debt_to_cover( match_price, feed_price, maintenance_collateral_ratio ), + ask.debt_type() ) ); asset call_pays, call_receives, order_pays, order_receives; if( usd_to_buy > usd_for_sale ) @@ -637,7 +654,8 @@ int database::match( const limit_order_object& bid, const call_order_object& ask int result = 0; result |= fill_limit_order( bid, order_pays, order_receives, cull_taker, match_price, false ); // the limit order is taker result |= fill_call_order( ask, call_pays, call_receives, match_price, true ) << 1; // the call order is maker - FC_ASSERT( result != 0 ); + // result can be 0 when call order has target_collateral_ratio option set. + return result; } @@ -834,14 +852,14 @@ bool database::fill_call_order( const call_order_object& order, const asset& pay }); const account_object& borrower = order.borrower(*this); - if( collateral_freed || pays.asset_id == asset_id_type() ) + if( collateral_freed.valid() || pays.asset_id == asset_id_type() ) { const account_statistics_object& borrower_statistics = borrower.statistics(*this); - if( collateral_freed ) + if( collateral_freed.valid() ) adjust_balance(borrower.get_id(), *collateral_freed); modify( borrower_statistics, [&]( account_statistics_object& b ){ - if( collateral_freed && collateral_freed->amount > 0 && collateral_freed->asset_id == asset_id_type()) + if( collateral_freed.valid() && collateral_freed->amount > 0 && collateral_freed->asset_id == asset_id_type() ) b.total_core_in_orders -= collateral_freed->amount; if( pays.asset_id == asset_id_type() ) b.total_core_in_orders -= pays.amount; @@ -854,7 +872,7 @@ bool database::fill_call_order( const call_order_object& order, const asset& pay push_applied_operation( fill_order_operation( order.id, order.borrower, pays, receives, asset(0, pays.asset_id), fill_price, is_maker ) ); - if( collateral_freed ) + if( collateral_freed.valid() ) remove( order ); return collateral_freed.valid(); @@ -950,6 +968,7 @@ bool database::check_call_orders(const asset_object& mia, bool enable_black_swan bool before_core_hardfork_343 = ( maint_time <= HARDFORK_CORE_343_TIME ); // update call_price after partially filled bool before_core_hardfork_453 = ( maint_time <= HARDFORK_CORE_453_TIME ); // multiple matching issue bool before_core_hardfork_606 = ( maint_time <= HARDFORK_CORE_606_TIME ); // feed always trigger call + bool before_core_hardfork_834 = ( maint_time <= HARDFORK_CORE_834_TIME ); // target collateral ratio option while( !check_for_blackswan( mia, enable_black_swan ) && call_itr != call_end ) { @@ -988,6 +1007,11 @@ bool database::check_call_orders(const asset_object& mia, bool enable_black_swan return true; } + if( !before_core_hardfork_834 ) + usd_to_buy.amount = call_itr->get_max_debt_to_cover( match_price, + bitasset.current_feed.settlement_price, + bitasset.current_feed.maintenance_collateral_ratio ); + asset call_pays, call_receives, order_pays, order_receives; if( usd_to_buy > usd_for_sale ) { // fill order @@ -1033,7 +1057,7 @@ bool database::check_call_orders(const asset_object& mia, bool enable_black_swan else order_receives = usd_to_buy.multiply_and_round_up( match_price ); // round up, in favor of limit order - filled_call = true; + filled_call = true; // this is safe, since BSIP38 (hard fork core-834) depends on BSIP31 (hard fork core-343) if( usd_to_buy == usd_for_sale ) filled_limit = true; diff --git a/libraries/chain/hardfork.d/CORE_834.hf b/libraries/chain/hardfork.d/CORE_834.hf new file mode 100644 index 0000000000..179c5be6bb --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_834.hf @@ -0,0 +1,4 @@ +// bitshares-core issue #834 "BSIP38: add target CR option to short positions" +#ifndef HARDFORK_CORE_834_TIME +#define HARDFORK_CORE_834_TIME (fc::time_point_sec( 1600000000 )) +#endif diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index bb4a432dd4..729f56af31 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -366,7 +366,8 @@ namespace graphene { namespace chain { */ ///@{ int match( const limit_order_object& taker, const limit_order_object& maker, const price& trade_price ); - int match( const limit_order_object& taker, const call_order_object& maker, const price& trade_price ); + int match( const limit_order_object& taker, const call_order_object& maker, const price& trade_price, + const price& feed_price, const uint16_t maintenance_collateral_ratio ); /// @return the amount of asset settled asset match(const call_order_object& call, const force_settlement_object& settle, diff --git a/libraries/chain/include/graphene/chain/market_object.hpp b/libraries/chain/include/graphene/chain/market_object.hpp index fae32a0028..4ece25f13e 100644 --- a/libraries/chain/include/graphene/chain/market_object.hpp +++ b/libraries/chain/include/graphene/chain/market_object.hpp @@ -125,12 +125,17 @@ class call_order_object : public abstract_object share_type debt; ///< call_price.quote.asset_id, access via get_debt price call_price; ///< Collateral / Debt + optional target_collateral_ratio; ///< maximum CR to maintain when selling collateral on margin call + pair get_market()const { auto tmp = std::make_pair( call_price.base.asset_id, call_price.quote.asset_id ); if( tmp.first > tmp.second ) std::swap( tmp.first, tmp.second ); return tmp; } + + /// Calculate maximum quantity of debt to cover to satisfy @ref target_collateral_ratio. + share_type get_max_debt_to_cover( price match_price, price feed_price, const uint16_t maintenance_collateral_ratio )const; }; /** @@ -259,7 +264,7 @@ FC_REFLECT_DERIVED( graphene::chain::limit_order_object, ) FC_REFLECT_DERIVED( graphene::chain::call_order_object, (graphene::db::object), - (borrower)(collateral)(debt)(call_price) ) + (borrower)(collateral)(debt)(call_price)(target_collateral_ratio) ) FC_REFLECT_DERIVED( graphene::chain::force_settlement_object, (graphene::db::object), diff --git a/libraries/chain/include/graphene/chain/protocol/market.hpp b/libraries/chain/include/graphene/chain/protocol/market.hpp index 5fd919b2cb..55438d7cc5 100644 --- a/libraries/chain/include/graphene/chain/protocol/market.hpp +++ b/libraries/chain/include/graphene/chain/protocol/market.hpp @@ -23,6 +23,7 @@ */ #pragma once #include +#include namespace graphene { namespace chain { @@ -94,8 +95,6 @@ namespace graphene { namespace chain { void validate()const; }; - - /** * @ingroup operations * @@ -110,6 +109,16 @@ namespace graphene { namespace chain { */ struct call_order_update_operation : public base_operation { + /** + * Options to be used in @ref call_order_update_operation. + * + * @note this struct can be expanded by adding more options in the end. + */ + struct options_type + { + optional target_collateral_ratio; ///< maximum CR to maintain when selling collateral on margin call + }; + /** this is slightly more expensive than limit orders, this pricing impacts prediction markets */ struct fee_parameters_type { uint64_t fee = 20 * GRAPHENE_BLOCKCHAIN_PRECISION; }; @@ -117,6 +126,8 @@ namespace graphene { namespace chain { account_id_type funding_account; ///< pays fee, collateral, and cover asset delta_collateral; ///< the amount of collateral to add to the margin position asset delta_debt; ///< the amount of the debt to be paid off, may be negative to issue new debt + + typedef extension extensions_type; // note: this will be jsonified to {...} but no longer [...] extensions_type extensions; account_id_type fee_payer()const { return funding_account; } @@ -214,6 +225,10 @@ FC_REFLECT( graphene::chain::bid_collateral_operation::fee_parameters_type, (fee 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::call_order_update_operation::options_type, (target_collateral_ratio) ) + +FC_REFLECT_TYPENAME( graphene::chain::call_order_update_operation::extensions_type ) + 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) ) diff --git a/libraries/chain/market_evaluator.cpp b/libraries/chain/market_evaluator.cpp index 48e9d38fe5..5fedbd02f8 100644 --- a/libraries/chain/market_evaluator.cpp +++ b/libraries/chain/market_evaluator.cpp @@ -157,6 +157,11 @@ void_result call_order_update_evaluator::do_evaluate(const call_order_update_ope { try { database& d = db(); + // TODO: remove this check and the assertion after hf_834 + if( d.get_dynamic_global_properties().next_maintenance_time <= HARDFORK_CORE_834_TIME ) + FC_ASSERT( !o.extensions.value.target_collateral_ratio.valid(), + "Can not set target_collateral_ratio in call_order_update_operation before hardfork 834." ); + _paying_account = &o.funding_account(d); _debt_asset = &o.delta_debt.asset_id(d); FC_ASSERT( _debt_asset->is_market_issued(), "Unable to cover ${sym} as it is not a collateralized asset.", @@ -222,6 +227,8 @@ void_result call_order_update_evaluator::do_apply(const call_order_update_operat optional old_collateralization; optional old_debt; + optional new_target_cr = o.extensions.value.target_collateral_ratio; + if( itr == call_idx.end() ) { FC_ASSERT( o.delta_collateral.amount > 0 ); @@ -233,7 +240,7 @@ void_result call_order_update_evaluator::do_apply(const call_order_update_operat call.debt = o.delta_debt.amount; call.call_price = price::call_price(o.delta_debt, o.delta_collateral, _bitasset_data->current_feed.maintenance_collateral_ratio); - + call.target_collateral_ratio = new_target_cr; }); } else @@ -243,13 +250,14 @@ void_result call_order_update_evaluator::do_apply(const call_order_update_operat old_debt = call_obj->debt; d.modify( *call_obj, [&]( call_order_object& call ){ - call.collateral += o.delta_collateral.amount; - call.debt += o.delta_debt.amount; - if( call.debt > 0 ) - { - call.call_price = price::call_price(call.get_debt(), call.get_collateral(), - _bitasset_data->current_feed.maintenance_collateral_ratio); - } + call.collateral += o.delta_collateral.amount; + call.debt += o.delta_debt.amount; + if( call.debt > 0 ) + { + call.call_price = price::call_price(call.get_debt(), call.get_collateral(), + _bitasset_data->current_feed.maintenance_collateral_ratio); + } + call.target_collateral_ratio = new_target_cr; }); } diff --git a/libraries/chain/market_object.cpp b/libraries/chain/market_object.cpp new file mode 100644 index 0000000000..993df7924f --- /dev/null +++ b/libraries/chain/market_object.cpp @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2018 Abit More, and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include + +#include + +using namespace graphene::chain; + +/* +target_CR = max( target_CR, MCR ) + +target_CR = new_collateral / ( new_debt / feed_price ) + = ( collateral - max_amount_to_sell ) * feed_price + / ( debt - amount_to_get ) + = ( collateral - max_amount_to_sell ) * feed_price + / ( debt - round_down(max_amount_to_sell * match_price ) ) + = ( collateral - max_amount_to_sell ) * feed_price + / ( debt - (max_amount_to_sell * match_price - x) ) + +Note: x is the fraction, 0 <= x < 1 + +=> + +max_amount_to_sell = ( (debt + x) * target_CR - collateral * feed_price ) + / (target_CR * match_price - feed_price) + = ( (debt + x) * tCR / DENOM - collateral * fp_debt_amt / fp_coll_amt ) + / ( (tCR / DENOM) * (mp_debt_amt / mp_coll_amt) - fp_debt_amt / fp_coll_amt ) + = ( (debt + x) * tCR * fp_coll_amt * mp_coll_amt - collateral * fp_debt_amt * DENOM * mp_coll_amt) + / ( tCR * mp_debt_amt * fp_coll_amt - fp_debt_amt * DENOM * mp_coll_amt ) + +max_debt_to_cover = max_amount_to_sell * match_price + = max_amount_to_sell * mp_debt_amt / mp_coll_amt + = ( (debt + x) * tCR * fp_coll_amt * mp_debt_amt - collateral * fp_debt_amt * DENOM * mp_debt_amt) + / (tCR * mp_debt_amt * fp_coll_amt - fp_debt_amt * DENOM * mp_coll_amt) +*/ +share_type call_order_object::get_max_debt_to_cover( price match_price, + price feed_price, + const uint16_t maintenance_collateral_ratio )const +{ try { + // be defensive here, make sure feed_price is in collateral / debt format + if( feed_price.base.asset_id != call_price.base.asset_id ) + feed_price = ~feed_price; + + FC_ASSERT( feed_price.base.asset_id == call_price.base.asset_id + && feed_price.quote.asset_id == call_price.quote.asset_id ); + + if( call_price > feed_price ) // feed protected. be defensive here, although this should be guaranteed by caller + return 0; + + if( !target_collateral_ratio.valid() ) // target cr is not set + return debt; + + uint16_t tcr = std::max( *target_collateral_ratio, maintenance_collateral_ratio ); // use mcr if target cr is too small + + // be defensive here, make sure match_price is in collateral / debt format + if( match_price.base.asset_id != call_price.base.asset_id ) + match_price = ~match_price; + + FC_ASSERT( match_price.base.asset_id == call_price.base.asset_id + && match_price.quote.asset_id == call_price.quote.asset_id ); + + typedef boost::multiprecision::int256_t i256; + i256 mp_debt_amt = match_price.quote.amount.value; + i256 mp_coll_amt = match_price.base.amount.value; + i256 fp_debt_amt = feed_price.quote.amount.value; + i256 fp_coll_amt = feed_price.base.amount.value; + + // firstly we calculate without the fraction (x), the result could be a bit too small + i256 numerator = fp_coll_amt * mp_debt_amt * debt.value * tcr + - fp_debt_amt * mp_debt_amt * collateral.value * GRAPHENE_COLLATERAL_RATIO_DENOM; + if( numerator < 0 ) // feed protected, actually should not be true here, just check to be safe + return 0; + + i256 denominator = fp_coll_amt * mp_debt_amt * tcr - fp_debt_amt * mp_coll_amt * GRAPHENE_COLLATERAL_RATIO_DENOM; + if( denominator <= 0 ) // black swan + return debt; + + // note: if add 1 here, will result in 1.5x imperfection rate; + // however, due to rounding, the result could still be a bit too big, thus imperfect. + i256 to_cover_i256 = ( numerator / denominator ); + if( to_cover_i256 >= debt.value ) // avoid possible overflow + return debt; + share_type to_cover_amt = static_cast< int64_t >( to_cover_i256 ); + + // stabilize + // note: rounding up-down results in 3x imperfection rate in comparison to down-down-up + asset to_pay = asset( to_cover_amt, debt_type() ) * match_price; + asset to_cover = to_pay * match_price; + to_pay = to_cover.multiply_and_round_up( match_price ); + + if( to_cover.amount >= debt || to_pay.amount >= collateral ) // to be safe + return debt; + FC_ASSERT( to_pay.amount < collateral && to_cover.amount < debt ); + + // check collateral ratio after filled, if it's OK, we return + price new_call_price = price::call_price( get_debt() - to_cover, get_collateral() - to_pay, tcr ); + if( new_call_price > feed_price ) + return to_cover.amount; + + // be here, to_cover is too small due to rounding. deal with the fraction + numerator += fp_coll_amt * mp_debt_amt * tcr; // plus the fraction + to_cover_i256 = ( numerator / denominator ) + 1; + if( to_cover_i256 >= debt.value ) // avoid possible overflow + to_cover_i256 = debt.value; + to_cover_amt = static_cast< int64_t >( to_cover_i256 ); + + asset max_to_pay = ( ( to_cover_amt == debt.value ) ? get_collateral() + : asset( to_cover_amt, debt_type() ).multiply_and_round_up( match_price ) ); + if( max_to_pay.amount > collateral ) + max_to_pay.amount = collateral; + + asset max_to_cover = ( ( max_to_pay.amount == collateral ) ? get_debt() : ( max_to_pay * match_price ) ); + if( max_to_cover.amount >= debt ) // to be safe + { + max_to_pay.amount = collateral; + max_to_cover.amount = debt; + } + + if( max_to_pay <= to_pay || max_to_cover <= to_cover ) // strange data. should skip binary search and go on, but doesn't help much + return debt; + FC_ASSERT( max_to_pay > to_pay && max_to_cover > to_cover ); + + asset min_to_pay = to_pay; + asset min_to_cover = to_cover; + + // try with binary search to find a good value + // note: actually binary search can not always provide perfect result here, + // due to rounding, collateral ratio is not always increasing while to_pay or to_cover is increasing + bool max_is_ok = false; + while( true ) + { + // get the mean + if( match_price.base.amount < match_price.quote.amount ) // step of collateral is smaller + { + to_pay.amount = ( min_to_pay.amount + max_to_pay.amount + 1 ) / 2; // should not overflow. round up here + if( to_pay.amount == max_to_pay.amount ) + to_cover.amount = max_to_cover.amount; + else + { + to_cover = to_pay * match_price; + if( to_cover.amount >= max_to_cover.amount ) // can be true when max_is_ok is false + { + to_pay.amount = max_to_pay.amount; + to_cover.amount = max_to_cover.amount; + } + else + { + to_pay = to_cover.multiply_and_round_up( match_price ); // stabilization, no change or become smaller + FC_ASSERT( to_pay.amount < max_to_pay.amount ); + } + } + } + else // step of debt is smaller or equal + { + to_cover.amount = ( min_to_cover.amount + max_to_cover.amount ) / 2; // should not overflow. round down here + if( to_cover.amount == max_to_cover.amount ) + to_pay.amount = max_to_pay.amount; + else + { + to_pay = to_cover.multiply_and_round_up( match_price ); + if( to_pay.amount >= max_to_pay.amount ) // can be true when max_is_ok is false + { + to_pay.amount = max_to_pay.amount; + to_cover.amount = max_to_cover.amount; + } + else + { + to_cover = to_pay * match_price; // stabilization, to_cover should have increased + if( to_cover.amount >= max_to_cover.amount ) // to be safe + { + to_pay.amount = max_to_pay.amount; + to_cover.amount = max_to_cover.amount; + } + } + } + } + + // check again to see if we've moved away from the minimums, if not, use the maximums directly + if( to_pay.amount <= min_to_pay.amount || to_cover.amount <= min_to_cover.amount + || to_pay.amount > max_to_pay.amount || to_cover.amount > max_to_cover.amount ) + { + to_pay.amount = max_to_pay.amount; + to_cover.amount = max_to_cover.amount; + } + + // check the mean + if( to_pay.amount == max_to_pay.amount && ( max_is_ok || to_pay.amount == collateral ) ) + return to_cover.amount; + FC_ASSERT( to_pay.amount < collateral && to_cover.amount < debt ); + + new_call_price = price::call_price( get_debt() - to_cover, get_collateral() - to_pay, tcr ); + if( new_call_price > feed_price ) // good + { + if( to_pay.amount == max_to_pay.amount ) + return to_cover.amount; + max_to_pay.amount = to_pay.amount; + max_to_cover.amount = to_cover.amount; + max_is_ok = true; + } + else // not good + { + if( to_pay.amount == max_to_pay.amount ) + break; + min_to_pay.amount = to_pay.amount; + min_to_cover.amount = to_cover.amount; + } + } + + // be here, max_to_cover is too small due to rounding. search forward + for( uint64_t d1 = 0, d2 = 1, d3 = 1; ; d1 = d2, d2 = d3, d3 = d1 + d2 ) // 1,1,2,3,5,8,... + { + if( match_price.base.amount > match_price.quote.amount ) // step of debt is smaller + { + to_pay.amount += d2; + if( to_pay.amount >= collateral ) + return debt; + to_cover = to_pay * match_price; + if( to_cover.amount >= debt ) + return debt; + to_pay = to_cover.multiply_and_round_up( match_price ); // stabilization + if( to_pay.amount >= collateral ) + return debt; + } + else // step of collateral is smaller or equal + { + to_cover.amount += d2; + if( to_cover.amount >= debt ) + return debt; + to_pay = to_cover.multiply_and_round_up( match_price ); + if( to_pay.amount >= collateral ) + return debt; + to_cover = to_pay * match_price; // stabilization + if( to_cover.amount >= debt ) + return debt; + } + + // check + FC_ASSERT( to_pay.amount < collateral && to_cover.amount < debt ); + + new_call_price = price::call_price( get_debt() - to_cover, get_collateral() - to_pay, tcr ); + if( new_call_price > feed_price ) // good + return to_cover.amount; + } + +} FC_CAPTURE_AND_RETHROW( (*this)(feed_price)(match_price)(maintenance_collateral_ratio) ) } diff --git a/libraries/chain/proposal_evaluator.cpp b/libraries/chain/proposal_evaluator.cpp index d858270c97..b80cb720f7 100644 --- a/libraries/chain/proposal_evaluator.cpp +++ b/libraries/chain/proposal_evaluator.cpp @@ -33,12 +33,22 @@ struct proposal_operation_hardfork_visitor { typedef void result_type; const fc::time_point_sec block_time; + const fc::time_point_sec next_maintenance_time; - proposal_operation_hardfork_visitor(const fc::time_point_sec t) : block_time(t) {} + proposal_operation_hardfork_visitor( const fc::time_point_sec bt, const fc::time_point_sec nmt ) + : block_time(bt), next_maintenance_time(nmt) {} template void operator()(const T &v) const {} + // TODO review and cleanup code below after hard fork + // hf_834 + void operator()(const graphene::chain::call_order_update_operation &v) const { + if (next_maintenance_time <= HARDFORK_CORE_834_TIME) { + FC_ASSERT( !v.extensions.value.target_collateral_ratio.valid(), + "Can not set target_collateral_ratio in call_order_update_operation before hardfork 834." ); + } + } // hf_620 void operator()(const graphene::chain::asset_create_operation &v) const { if (block_time < HARDFORK_CORE_620_TIME) { @@ -107,7 +117,8 @@ void_result proposal_create_evaluator::do_evaluate(const proposal_create_operati // Calling the proposal hardfork visitor const fc::time_point_sec block_time = d.head_block_time(); - proposal_operation_hardfork_visitor vtor(block_time); + const fc::time_point_sec next_maint_time = d.get_dynamic_global_properties().next_maintenance_time; + proposal_operation_hardfork_visitor vtor( block_time, next_maint_time ); vtor( o ); if( block_time < HARDFORK_CORE_214_TIME ) { // cannot be removed after hf, unfortunately diff --git a/libraries/chain/protocol/market.cpp b/libraries/chain/protocol/market.cpp index be13b8ba35..fd12fa4e7d 100644 --- a/libraries/chain/protocol/market.cpp +++ b/libraries/chain/protocol/market.cpp @@ -43,6 +43,9 @@ void call_order_update_operation::validate()const FC_ASSERT( fee.amount >= 0 ); FC_ASSERT( delta_collateral.asset_id != delta_debt.asset_id ); FC_ASSERT( delta_collateral.amount != 0 || delta_debt.amount != 0 ); + + // note: no validation is needed for extensions so far: the only attribute inside is target_collateral_ratio + } FC_CAPTURE_AND_RETHROW((*this)) } void bid_collateral_operation::validate()const diff --git a/libraries/wallet/include/graphene/wallet/wallet.hpp b/libraries/wallet/include/graphene/wallet/wallet.hpp index 86e59e7e60..deaa6588c8 100644 --- a/libraries/wallet/include/graphene/wallet/wallet.hpp +++ b/libraries/wallet/include/graphene/wallet/wallet.hpp @@ -971,6 +971,26 @@ class wallet_api signed_transaction borrow_asset(string borrower_name, string amount_to_borrow, string asset_symbol, string amount_of_collateral, bool broadcast = false); + /** Borrow an asset or update the debt/collateral ratio for the loan, with additional options. + * + * This is the first step in shorting an asset. Call \c sell_asset() to complete the short. + * + * @param borrower_name the name or id of the account associated with the transaction. + * @param amount_to_borrow the amount of the asset being borrowed. Make this value + * negative to pay back debt. + * @param asset_symbol the symbol or id of the asset being borrowed. + * @param amount_of_collateral the amount of the backing asset to add to your collateral + * position. Make this negative to claim back some of your collateral. + * The backing asset is defined in the \c bitasset_options for the asset being borrowed. + * @param extensions additional options + * @param broadcast true to broadcast the transaction on the network + * @returns the signed transaction borrowing the asset + */ + signed_transaction borrow_asset_ext( string borrower_name, string amount_to_borrow, string asset_symbol, + string amount_of_collateral, + call_order_update_operation::extensions_type extensions, + bool broadcast = false ); + /** Cancel an existing order * * @param order_id the id of order to be cancelled @@ -1688,6 +1708,7 @@ FC_API( graphene::wallet::wallet_api, (create_account_with_brain_key) (sell_asset) (borrow_asset) + (borrow_asset_ext) (cancel_order) (transfer) (transfer2) diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index 64c30fa288..44ab19747c 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -2027,6 +2027,31 @@ class wallet_api_impl return sign_transaction(trx, broadcast); } + signed_transaction borrow_asset_ext( string seller_name, string amount_to_borrow, string asset_symbol, + string amount_of_collateral, + call_order_update_operation::extensions_type extensions, + bool broadcast = false) + { + account_object seller = get_account(seller_name); + asset_object mia = get_asset(asset_symbol); + FC_ASSERT(mia.is_market_issued()); + asset_object collateral = get_asset(get_object(*mia.bitasset_data_id).options.short_backing_asset); + + call_order_update_operation op; + op.funding_account = seller.id; + op.delta_debt = mia.amount_from_string(amount_to_borrow); + op.delta_collateral = collateral.amount_from_string(amount_of_collateral); + op.extensions = extensions; + + signed_transaction trx; + trx.operations = {op}; + set_operation_fees( trx, _remote_db->get_global_properties().parameters.current_fees); + trx.validate(); + idump((broadcast)); + + return sign_transaction(trx, broadcast); + } + signed_transaction cancel_order(object_id_type order_id, bool broadcast = false) { try { FC_ASSERT(!is_locked()); @@ -3938,6 +3963,15 @@ signed_transaction wallet_api::borrow_asset(string seller_name, string amount_to return my->borrow_asset(seller_name, amount_to_sell, asset_symbol, amount_of_collateral, broadcast); } +signed_transaction wallet_api::borrow_asset_ext( string seller_name, string amount_to_sell, + string asset_symbol, string amount_of_collateral, + call_order_update_operation::extensions_type extensions, + bool broadcast) +{ + FC_ASSERT(!is_locked()); + return my->borrow_asset_ext(seller_name, amount_to_sell, asset_symbol, amount_of_collateral, extensions, broadcast); +} + signed_transaction wallet_api::cancel_order(object_id_type order_id, bool broadcast) { FC_ASSERT(!is_locked()); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c4a4310c83..98bc12622c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,18 +7,18 @@ if( GPERFTOOLS_FOUND ) endif() file(GLOB UNIT_TESTS "tests/*.cpp") -add_executable( chain_test ${UNIT_TESTS} ${COMMON_SOURCES} ) +add_executable( chain_test ${COMMON_SOURCES} ${UNIT_TESTS} ) target_link_libraries( chain_test graphene_chain graphene_app graphene_account_history graphene_egenesis_none fc graphene_wallet ${PLATFORM_SPECIFIC_LIBS} ) if(MSVC) set_source_files_properties( tests/serialization_tests.cpp PROPERTIES COMPILE_FLAGS "/bigobj" ) endif(MSVC) file(GLOB PERFORMANCE_TESTS "performance/*.cpp") -add_executable( performance_test ${PERFORMANCE_TESTS} ${COMMON_SOURCES} ) +add_executable( performance_test ${COMMON_SOURCES} ${PERFORMANCE_TESTS} ) target_link_libraries( performance_test graphene_chain graphene_app graphene_account_history graphene_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} ) file(GLOB BENCH_MARKS "benchmarks/*.cpp") -add_executable( chain_bench ${BENCH_MARKS} ${COMMON_SOURCES} ) +add_executable( chain_bench ${COMMON_SOURCES} ${BENCH_MARKS} ) target_link_libraries( chain_bench graphene_chain graphene_app graphene_account_history graphene_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} ) file(GLOB APP_SOURCES "app/*.cpp") diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index 1ec8425677..72311faf5b 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -876,7 +876,8 @@ operation_result database_fixture::force_settle( const account_object& who, asse return op_result; } FC_CAPTURE_AND_RETHROW( (who)(what) ) } -const call_order_object* database_fixture::borrow(const account_object& who, asset what, asset collateral) +const call_order_object* database_fixture::borrow( const account_object& who, asset what, asset collateral, + optional target_cr ) { try { set_expiration( db, trx ); trx.operations.clear(); @@ -884,6 +885,7 @@ const call_order_object* database_fixture::borrow(const account_object& who, ass update.funding_account = who.id; update.delta_collateral = collateral; update.delta_debt = what; + update.extensions.value.target_collateral_ratio = target_cr; trx.operations.push_back(update); for( auto& op : trx.operations ) db.current_fee_schedule().set_fee(op); trx.validate(); @@ -898,9 +900,9 @@ const call_order_object* database_fixture::borrow(const account_object& who, ass if( itr != call_idx.end() ) call_obj = &*itr; return call_obj; -} FC_CAPTURE_AND_RETHROW( (who.name)(what)(collateral) ) } +} FC_CAPTURE_AND_RETHROW( (who.name)(what)(collateral)(target_cr) ) } -void database_fixture::cover(const account_object& who, asset what, asset collateral) +void database_fixture::cover(const account_object& who, asset what, asset collateral, optional target_cr) { try { set_expiration( db, trx ); trx.operations.clear(); @@ -908,13 +910,14 @@ void database_fixture::cover(const account_object& who, asset what, asset collat update.funding_account = who.id; update.delta_collateral = -collateral; update.delta_debt = -what; + update.extensions.value.target_collateral_ratio = target_cr; trx.operations.push_back(update); 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)(what)(collateral) ) } +} FC_CAPTURE_AND_RETHROW( (who.name)(what)(collateral)(target_cr) ) } void database_fixture::bid_collateral(const account_object& who, const asset& to_bid, const asset& to_cover) { try { diff --git a/tests/common/database_fixture.hpp b/tests/common/database_fixture.hpp index bd7611d625..f5511d3b8c 100644 --- a/tests/common/database_fixture.hpp +++ b/tests/common/database_fixture.hpp @@ -210,12 +210,17 @@ struct database_fixture { void publish_feed(asset_id_type mia, account_id_type by, const price_feed& f) { publish_feed(mia(db), by(db), f); } 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); } - const call_order_object* borrow(const account_object& who, asset what, asset collateral); - 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); + + const call_order_object* borrow( account_id_type who, asset what, asset collateral, + optional target_cr = {} ) + { return borrow(who(db), what, collateral, target_cr); } + const call_order_object* borrow( const account_object& who, asset what, asset collateral, + optional target_cr = {} ); + void cover(account_id_type who, asset what, asset collateral_freed, + optional target_cr = {} ) + { cover(who(db), what, collateral_freed, target_cr); } + void cover(const account_object& who, asset what, asset collateral_freed, + optional target_cr = {} ); void bid_collateral(const account_object& who, const asset& to_bid, const asset& to_cover); const asset_object& get_asset( const string& symbol )const; diff --git a/tests/tests/basic_tests.cpp b/tests/tests/basic_tests.cpp index d32795edb6..7f5b4b0089 100644 --- a/tests/tests/basic_tests.cpp +++ b/tests/tests/basic_tests.cpp @@ -295,6 +295,63 @@ BOOST_AUTO_TEST_CASE( price_test ) BOOST_CHECK(dummy == dummy2); } +BOOST_AUTO_TEST_CASE( price_multiplication_test ) +{ try { + // random test + std::mt19937_64 gen( time(NULL) ); + std::uniform_int_distribution amt_uid(1, GRAPHENE_MAX_SHARE_SUPPLY); + std::uniform_int_distribution amt_uid2(1, 1000*1000*1000); + std::uniform_int_distribution amt_uid3(1, 1000*1000); + std::uniform_int_distribution amt_uid4(1, 1000); + asset a; + price p; + for( int i = 1*1000*1000; i > 0; --i ) + { + if( i <= 30 ) + a = asset( 0 ); + else if( i % 4 == 0 ) + a = asset( amt_uid(gen) ); + else if( i % 4 == 1 ) + a = asset( amt_uid2(gen) ); + else if( i % 4 == 2 ) + a = asset( amt_uid3(gen) ); + else // if( i % 4 == 3 ) + a = asset( amt_uid4(gen) ); + + if( i % 7 == 0 ) + p = price( asset(amt_uid(gen)), asset(amt_uid(gen), asset_id_type(1)) ); + else if( i % 7 == 1 ) + p = price( asset(amt_uid2(gen)), asset(amt_uid2(gen), asset_id_type(1)) ); + else if( i % 7 == 2 ) + p = price( asset(amt_uid3(gen)), asset(amt_uid3(gen), asset_id_type(1)) ); + else if( i % 7 == 3 ) + p = price( asset(amt_uid4(gen)), asset(amt_uid4(gen), asset_id_type(1)) ); + else if( i % 7 == 4 ) + p = price( asset(amt_uid(gen)), asset(amt_uid(gen), asset_id_type(1)) ); + else if( i % 7 == 5 ) + p = price( asset(amt_uid4(gen)), asset(amt_uid2(gen), asset_id_type(1)) ); + else // if( i % 7 == 6 ) + p = price( asset(amt_uid2(gen)), asset(amt_uid4(gen), asset_id_type(1)) ); + + try + { + asset b = a * p; + asset a1 = b.multiply_and_round_up( p ); + BOOST_CHECK( a1 <= a ); + BOOST_CHECK( (a1 * p) == b ); + + b = a.multiply_and_round_up( p ); + a1 = b * p; + BOOST_CHECK( a1 >= a ); + BOOST_CHECK( a1.multiply_and_round_up( p ) == b ); + } + catch( fc::assert_exception& e ) + { + BOOST_CHECK( e.to_detail_string().find( "result <= GRAPHENE_MAX_SHARE_SUPPLY" ) != string::npos ); + } + } +} FC_LOG_AND_RETHROW() } + BOOST_AUTO_TEST_CASE( memo_test ) { try { memo_data m; diff --git a/tests/tests/call_order_tests.cpp b/tests/tests/call_order_tests.cpp new file mode 100644 index 0000000000..1d883bb23c --- /dev/null +++ b/tests/tests/call_order_tests.cpp @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2018 Abit More, and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include + +#include + +#include "../common/database_fixture.hpp" + +using namespace graphene::chain; +using namespace graphene::chain::test; + +BOOST_FIXTURE_TEST_SUITE( call_order_tests, database_fixture ) + +BOOST_AUTO_TEST_CASE( call_order_object_test ) +{ try { + // assume GRAPHENE_COLLATERAL_RATIO_DENOM is 1000 in this test case + BOOST_REQUIRE_EQUAL( 1000, GRAPHENE_COLLATERAL_RATIO_DENOM ); + + // function to create a new call_order_object + auto new_call_obj = []( const share_type c, const share_type d, int16_t mcr, optional tcr = {} ) { + call_order_object o; + o.collateral = c; + o.debt = d; + o.call_price = price::call_price( asset( d, asset_id_type(1)), asset(c) , mcr ); + o.target_collateral_ratio = tcr; + return o; + }; + + // function to validate result of call_order_object::get_max_debt_to_cover(...) + auto validate_result = []( const call_order_object& o, const price& match_price, const price& feed_price, + int16_t mcr, const share_type result, bool print_log = true ) { + if( result == 0 ) + return 1; + + BOOST_REQUIRE_GT( result.value, 0 ); + BOOST_REQUIRE_LE( result.value, o.debt.value ); + + BOOST_REQUIRE( match_price.base.asset_id == o.collateral_type() ); + BOOST_REQUIRE( match_price.quote.asset_id == o.debt_type() ); + BOOST_REQUIRE( feed_price.base.asset_id == o.collateral_type() ); + BOOST_REQUIRE( feed_price.quote.asset_id == o.debt_type() ); + + // should be in margin call territory + price call_price = price::call_price( o.get_debt(), o.get_collateral(), mcr ); + BOOST_CHECK( call_price <= feed_price ); + + if( !o.target_collateral_ratio.valid() ) + { + BOOST_CHECK_EQUAL( result.value, o.debt.value ); + return 2; + } + + auto tcr = *o.target_collateral_ratio; + if( tcr == 0 ) + tcr = 1; + + asset to_cover( result, o.debt_type() ); + asset to_pay = o.get_collateral(); + if( result < o.debt ) + { + to_pay = to_cover.multiply_and_round_up( match_price ); + BOOST_CHECK_LT( to_pay.amount.value, o.collateral.value ); // should cover more on black swan event + BOOST_CHECK_EQUAL( result.value, (to_pay * match_price).amount.value ); // should not change after rounded down debt to cover + + // should have target_cr set + // after sold some collateral, the collateral ratio will be higher than expected + price new_tcr_call_price = price::call_price( o.get_debt() - to_cover, o.get_collateral() - to_pay, tcr ); + price new_mcr_call_price = price::call_price( o.get_debt() - to_cover, o.get_collateral() - to_pay, mcr ); + BOOST_CHECK( new_tcr_call_price > feed_price ); + BOOST_CHECK( new_mcr_call_price > feed_price ); + } + + // if sell less than calculated, the collateral ratio will not be higher than expected + int j = 3; + for( int i = 100000; i >= 10; i /= 10, ++j ) + { + int total_passes = 3; + for( int k = 1; k <= total_passes; ++k ) + { + bool last_check = (k == total_passes); + asset sell_less = to_pay; + asset cover_less; + for( int m = 0; m < k; ++m ) + { + if( i == 100000 ) + sell_less.amount -= 1; + else + sell_less.amount -= ( ( sell_less.amount + i - 1 ) / i ); + cover_less = sell_less * match_price; // round down debt to cover + if( cover_less >= to_cover ) + { + cover_less.amount = to_cover.amount - 1; + sell_less = cover_less * match_price; // round down collateral + cover_less = sell_less * match_price; // round down debt to cover + } + sell_less = cover_less.multiply_and_round_up( match_price ); // round up to get collateral to sell + if( sell_less.amount <= 0 || cover_less.amount <= 0 ) // unable to sell or cover less, we return + { + if( to_pay.amount == o.collateral ) + return j; + return (j + 10); + } + } + BOOST_REQUIRE_LT( cover_less.amount.value, o.debt.value ); + BOOST_REQUIRE_LT( sell_less.amount.value, o.collateral.value ); + price tmp_tcr_call_price = price::call_price( o.get_debt() - cover_less, o.get_collateral() - sell_less, tcr ); + price tmp_mcr_call_price = price::call_price( o.get_debt() - cover_less, o.get_collateral() - sell_less, mcr ); + bool cover_less_is_enough = ( tmp_tcr_call_price > feed_price && tmp_mcr_call_price > feed_price ); + if( !cover_less_is_enough ) + { + if( !last_check ) + continue; + if( to_pay.amount == o.collateral ) + return j; + return (j + 10); + } + if( print_log ) + { + print_log = false; + wlog( "Impefect result >= 1 / ${i}", ("i",i) ); + wdump( (o)(match_price)(feed_price)(mcr)(result)(sell_less)(cover_less)(tmp_mcr_call_price)(tmp_tcr_call_price) ); + } + break; + } + } + if( to_pay.amount == o.collateral ) + return j; + return (j + 10); + }; + + // init + int16_t mcr = 1750; + price mp, fp; + call_order_object obj; + int64_t expected; + share_type result; + + mp = price( asset(1100), asset(1000, asset_id_type(1)) ); // match_price + fp = price( asset(1000), asset(1000, asset_id_type(1)) ); // feed_price + + // fixed tests + obj = new_call_obj( 1751, 1000, mcr ); // order is not in margin call territory + expected = 0; + result = obj.get_max_debt_to_cover( mp, fp, mcr ); + BOOST_CHECK_EQUAL( result.value, expected ); + validate_result( obj, mp, fp, mcr, result ); + + obj = new_call_obj( 1751, 1000, mcr, 10000 ); // order is not in margin call territory + expected = 0; + result = obj.get_max_debt_to_cover( mp, fp, mcr ); + BOOST_CHECK_EQUAL( result.value, expected ); + validate_result( obj, mp, fp, mcr, result ); + + obj = new_call_obj( 160, 100, mcr ); // target_cr is not set + expected = 100; + result = obj.get_max_debt_to_cover( mp, fp, mcr ); + BOOST_CHECK_EQUAL( result.value, expected ); + validate_result( obj, mp, fp, mcr, result ); + + obj = new_call_obj( 1009, 1000, mcr, 200 ); // target_cr set, but order is in black swan territory + expected = 1000; + result = obj.get_max_debt_to_cover( mp, fp, mcr ); + BOOST_CHECK_EQUAL( result.value, expected ); + validate_result( obj, mp, fp, mcr, result ); + + obj = new_call_obj( 1499, 999, mcr, 1600 ); // target_cr is 160%, less than 175%, so use 175% + expected = 385; + result = obj.get_max_debt_to_cover( mp, fp, mcr ); + BOOST_CHECK_EQUAL( result.value, expected ); + validate_result( obj, mp, fp, mcr, result ); + + obj = new_call_obj( 1500, 1000, mcr, 1800 ); // target_cr is 180% + expected = 429; + result = obj.get_max_debt_to_cover( mp, fp, mcr ); + BOOST_CHECK_EQUAL( result.value, expected ); + validate_result( obj, mp, fp, mcr, result ); + + obj = new_call_obj( 1501, 1001, mcr, 2000 ); // target_cr is 200% + expected = 558; + result = obj.get_max_debt_to_cover( mp, fp, mcr ); + BOOST_CHECK_EQUAL( result.value, expected ); + validate_result( obj, mp, fp, mcr, result ); + + obj = new_call_obj( 1502, 1002, mcr, 3000 ); // target_cr is 300% + expected = 793; + result = obj.get_max_debt_to_cover( mp, fp, mcr ); + BOOST_CHECK_EQUAL( result.value, expected ); + validate_result( obj, mp, fp, mcr, result ); + + mcr = 1750; + mp = price( asset(40009), asset(79070, asset_id_type(1)) ); // match_price + fp = price( asset(40009), asset(86977, asset_id_type(1)) ); // feed_price + + obj = new_call_obj( 557197, 701502, mcr, 1700 ); // target_cr is less than mcr + result = obj.get_max_debt_to_cover( mp, fp, mcr ); + validate_result( obj, mp, fp, mcr, result ); + + mcr = 1455; + mp = price( asset(1150171), asset(985450, asset_id_type(1)) ); // match_price + fp = price( asset(418244), asset(394180, asset_id_type(1)) ); // feed_price + + obj = new_call_obj( 423536, 302688, mcr, 200 ); // target_cr is less than mcr + result = obj.get_max_debt_to_cover( mp, fp, mcr ); + validate_result( obj, mp, fp, mcr, result ); + + // random tests + std::mt19937_64 gen( time(NULL) ); + std::uniform_int_distribution amt_uid(1, GRAPHENE_MAX_SHARE_SUPPLY); + std::uniform_int_distribution amt_uid2(1, 1000*1000*1000); + std::uniform_int_distribution amt_uid3(1, 1000*1000); + std::uniform_int_distribution amt_uid4(1, 300); + std::uniform_int_distribution mp_num_uid(800, 1100); + std::uniform_int_distribution mcr_uid(1001, 32767); + std::uniform_int_distribution mcr_uid2(1001, 3000); + std::uniform_int_distribution tcr_uid(0, 65535); + std::uniform_int_distribution tcr_uid2(0, 3000); + + vector count(20,0); + int total = 500*1000; + for( int i = total; i > 0; --i ) + { + if( i % 9 == 0 ) + mcr = 1002; + else if( i % 3 == 0 ) + mcr = 1750; + else if( i % 3 == 1 ) + mcr = mcr_uid(gen); + else // if( i % 3 == 2 ) + mcr = mcr_uid2(gen); + + // call_object + if( i % 17 <= 0 ) + obj = new_call_obj( amt_uid(gen), amt_uid(gen), mcr, tcr_uid(gen) ); + else if( i % 17 <= 2 ) + obj = new_call_obj( amt_uid2(gen), amt_uid2(gen), mcr, tcr_uid(gen) ); + else if( i % 17 <= 3 ) + obj = new_call_obj( amt_uid3(gen), amt_uid3(gen), mcr, tcr_uid(gen) ); + else if( i % 17 <= 4 ) + obj = new_call_obj( amt_uid4(gen), amt_uid4(gen), mcr, tcr_uid(gen) ); + else if( i % 17 <= 5 ) + obj = new_call_obj( amt_uid(gen), amt_uid(gen), mcr, tcr_uid2(gen) ); + else if( i % 17 <= 7 ) + obj = new_call_obj( amt_uid2(gen), amt_uid2(gen), mcr, tcr_uid2(gen) ); + else if( i % 17 <= 8 ) + obj = new_call_obj( amt_uid3(gen), amt_uid3(gen), mcr, tcr_uid2(gen) ); + else if( i % 17 <= 9 ) + obj = new_call_obj( amt_uid4(gen), amt_uid4(gen), mcr, tcr_uid2(gen) ); + else if( i % 17 <= 11 ) + obj = new_call_obj( amt_uid3(gen), amt_uid2(gen), mcr, tcr_uid2(gen) ); + else if( i % 17 <= 12 ) + obj = new_call_obj( amt_uid2(gen), amt_uid3(gen), mcr, tcr_uid2(gen) ); + else if( i % 17 <= 13 ) + obj = new_call_obj( amt_uid4(gen), amt_uid2(gen), mcr, tcr_uid2(gen) ); + else if( i % 17 <= 14 ) + obj = new_call_obj( amt_uid2(gen), amt_uid4(gen), mcr, tcr_uid2(gen) ); + else if( i % 17 <= 15 ) + obj = new_call_obj( amt_uid3(gen), amt_uid4(gen), mcr, tcr_uid2(gen) ); + else // if( i % 17 <= 16 ) + obj = new_call_obj( amt_uid4(gen), amt_uid3(gen), mcr, tcr_uid2(gen) ); + + // call_price + price cp = price::call_price( obj.get_debt(), obj.get_collateral(), mcr ); + + // get feed_price, and make sure we have sufficient good samples + int retry = 20; + do { + if( i % 5 == 0 ) + fp = price( asset(amt_uid(gen)), asset(amt_uid(gen), asset_id_type(1)) ); + else if( i % 5 == 1 ) + fp = price( asset(amt_uid2(gen)), asset(amt_uid2(gen), asset_id_type(1)) ); + else if( i % 5 == 2 ) + fp = price( asset(amt_uid3(gen)), asset(amt_uid3(gen), asset_id_type(1)) ); + else if( i % 25 <= 18 ) + fp = price( asset(amt_uid4(gen)), asset(amt_uid4(gen), asset_id_type(1)) ); + else if( i % 25 == 19 ) + fp = price( asset(amt_uid2(gen)), asset(amt_uid3(gen), asset_id_type(1)) ); + else if( i % 25 == 20 ) + fp = price( asset(amt_uid3(gen)), asset(amt_uid2(gen), asset_id_type(1)) ); + else if( i % 25 == 21 ) + fp = price( asset(amt_uid3(gen)), asset(amt_uid4(gen), asset_id_type(1)) ); + else if( i % 25 == 22 ) + fp = price( asset(amt_uid4(gen)), asset(amt_uid3(gen), asset_id_type(1)) ); + else if( i % 25 == 23 ) + fp = price( asset(amt_uid4(gen)), asset(amt_uid2(gen), asset_id_type(1)) ); + else // if( i % 25 == 24 ) + fp = price( asset(amt_uid2(gen)), asset(amt_uid4(gen), asset_id_type(1)) ); + --retry; + } while( retry > 0 && ( cp > fp || cp < ( fp / ratio_type( mcr, 1000 ) ) ) ); + + // match_price + if( i % 16 == 0 ) + mp = fp * ratio_type( 1001, 1000 ); + else if( i % 4 == 0 ) + mp = fp * ratio_type( 1100, 1000 ); + else if( i % 4 == 1 ) + mp = fp * ratio_type( mp_num_uid(gen) , 1000 ); + else if( i % 8 == 4 ) + mp = price( asset(amt_uid2(gen)), asset(amt_uid3(gen), asset_id_type(1)) ); + else if( i % 8 == 5 ) + mp = price( asset(amt_uid3(gen)), asset(amt_uid2(gen), asset_id_type(1)) ); + else if( i % 8 == 6 ) + mp = price( asset(amt_uid2(gen)), asset(amt_uid2(gen), asset_id_type(1)) ); + else // if( i % 8 == 7 ) + mp = price( asset(amt_uid(gen)), asset(amt_uid(gen), asset_id_type(1)) ); + + try { + result = obj.get_max_debt_to_cover( mp, fp, mcr ); + auto vr = validate_result( obj, mp, fp, mcr, result, false ); + ++count[vr]; + } + catch( fc::assert_exception& e ) + { + BOOST_CHECK( e.to_detail_string().find( "result <= GRAPHENE_MAX_SHARE_SUPPLY" ) != string::npos ); + ++count[0]; + } + } + ilog( "count: [bad_input,sell zero,not set," + " sell full (perfect), sell full (<0.01%), sell full (<0.1%),sell full (<1%), sell full (other), ...," + " sell some (perfect), sell some (<0.01%), sell some (<0.1%),sell some (<1%), sell some (other), ... ]" ); + idump( (total)(count) ); + +} FC_CAPTURE_LOG_AND_RETHROW( (0) ) } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/tests/market_tests.cpp b/tests/tests/market_tests.cpp index c81380997e..1f29f0c843 100644 --- a/tests/tests/market_tests.cpp +++ b/tests/tests/market_tests.cpp @@ -1190,4 +1190,321 @@ BOOST_AUTO_TEST_CASE(hard_fork_343_cross_test) } FC_LOG_AND_RETHROW() } +/*** + * BSIP38 "target_collateral_ratio" test: matching a taker limit order with multiple maker call orders + */ +BOOST_AUTO_TEST_CASE(target_cr_test_limit_call) +{ try { + auto mi = db.get_global_properties().parameters.maintenance_interval; + generate_blocks(HARDFORK_CORE_834_TIME - mi); + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + + set_expiration( db, trx ); + + ACTORS((buyer)(buyer2)(buyer3)(seller)(borrower)(borrower2)(borrower3)(feedproducer)); + + const auto& bitusd = create_bitasset("USDBIT", feedproducer_id); + const auto& core = asset_id_type()(db); + + int64_t init_balance(1000000); + + transfer(committee_account, buyer_id, asset(init_balance)); + transfer(committee_account, buyer2_id, asset(init_balance)); + transfer(committee_account, buyer3_id, asset(init_balance)); + transfer(committee_account, borrower_id, asset(init_balance)); + transfer(committee_account, borrower2_id, asset(init_balance)); + transfer(committee_account, borrower3_id, asset(init_balance)); + update_feed_producers( bitusd, {feedproducer.id} ); + + price_feed current_feed; + current_feed.maintenance_collateral_ratio = 1750; + current_feed.maximum_short_squeeze_ratio = 1100; + current_feed.settlement_price = bitusd.amount( 1 ) / core.amount(5); + publish_feed( bitusd, feedproducer, current_feed ); + // start out with 300% collateral, call price is 15/1.75 CORE/USD = 60/7, tcr 170% is lower than 175% + const call_order_object& call = *borrow( borrower, bitusd.amount(1000), asset(15000), 1700); + call_order_id_type call_id = call.id; + // create another position with 310% collateral, call price is 15.5/1.75 CORE/USD = 62/7, tcr 200% is higher than 175% + const call_order_object& call2 = *borrow( borrower2, bitusd.amount(1000), asset(15500), 2000); + call_order_id_type call2_id = call2.id; + // create yet another position with 500% collateral, call price is 25/1.75 CORE/USD = 100/7, no tcr + const call_order_object& call3 = *borrow( borrower3, bitusd.amount(1000), asset(25000)); + transfer(borrower, seller, bitusd.amount(1000)); + transfer(borrower2, seller, bitusd.amount(1000)); + transfer(borrower3, seller, bitusd.amount(1000)); + + BOOST_CHECK_EQUAL( 1000, call.debt.value ); + BOOST_CHECK_EQUAL( 15000, call.collateral.value ); + BOOST_CHECK_EQUAL( 1000, call2.debt.value ); + BOOST_CHECK_EQUAL( 15500, call2.collateral.value ); + BOOST_CHECK_EQUAL( 1000, call3.debt.value ); + BOOST_CHECK_EQUAL( 25000, call3.collateral.value ); + BOOST_CHECK_EQUAL( 3000, get_balance(seller, bitusd) ); + BOOST_CHECK_EQUAL( 0, get_balance(seller, core) ); + BOOST_CHECK_EQUAL( 3000, get_balance(seller, bitusd) ); + BOOST_CHECK_EQUAL( init_balance - 15000, get_balance(borrower, core) ); + BOOST_CHECK_EQUAL( init_balance - 15500, get_balance(borrower2, core) ); + BOOST_CHECK_EQUAL( init_balance - 25000, get_balance(borrower3, core) ); + BOOST_CHECK_EQUAL( 0, get_balance(borrower, bitusd) ); + BOOST_CHECK_EQUAL( 0, get_balance(borrower2, bitusd) ); + BOOST_CHECK_EQUAL( 0, get_balance(borrower3, bitusd) ); + + // adjust price feed to get call and call2 (but not call3) into margin call territory + current_feed.settlement_price = bitusd.amount( 1 ) / core.amount(10); + publish_feed( bitusd, feedproducer, current_feed ); + // settlement price = 1/10, mssp = 1/11 + + // This sell order above MSSP will not be matched with a call + limit_order_id_type sell_high = create_sell_order(seller, bitusd.amount(7), core.amount(78))->id; + BOOST_CHECK_EQUAL( db.find( sell_high )->for_sale.value, 7 ); + + BOOST_CHECK_EQUAL( 2993, get_balance(seller, bitusd) ); + BOOST_CHECK_EQUAL( 0, get_balance(seller, core) ); + + // This buy order is too low will not be matched with a sell order + limit_order_id_type buy_low = create_sell_order(buyer, asset(80), bitusd.amount(10))->id; + // This buy order at MSSP will be matched only if no margin call (margin call takes precedence) + limit_order_id_type buy_med = create_sell_order(buyer2, asset(33000), bitusd.amount(3000))->id; + // This buy order above MSSP will be matched with a sell order (limit order with better price takes precedence) + limit_order_id_type buy_high = create_sell_order(buyer3, asset(111), bitusd.amount(10))->id; + + BOOST_CHECK_EQUAL( 0, get_balance(buyer, bitusd) ); + BOOST_CHECK_EQUAL( 0, get_balance(buyer2, bitusd) ); + BOOST_CHECK_EQUAL( 0, get_balance(buyer3, bitusd) ); + BOOST_CHECK_EQUAL( init_balance - 80, get_balance(buyer, core) ); + BOOST_CHECK_EQUAL( init_balance - 33000, get_balance(buyer2, core) ); + BOOST_CHECK_EQUAL( init_balance - 111, get_balance(buyer3, core) ); + + // call and call2's CR is quite high, and debt amount is quite a lot, assume neither of them will be completely filled + price match_price( bitusd.amount(1) / core.amount(11) ); + share_type call_to_cover = call_id(db).get_max_debt_to_cover(match_price,current_feed.settlement_price,1750); + share_type call2_to_cover = call2_id(db).get_max_debt_to_cover(match_price,current_feed.settlement_price,1750); + BOOST_CHECK_LT( call_to_cover.value, call_id(db).debt.value ); + BOOST_CHECK_LT( call2_to_cover.value, call2_id(db).debt.value ); + // even though call2 has a higher CR, since call's TCR is less than call2's TCR, so we expect call will cover less when called + BOOST_CHECK_LT( call_to_cover.value, call2_to_cover.value ); + + // Create a big sell order slightly below the call price, will be matched with several orders + BOOST_CHECK( !create_sell_order(seller, bitusd.amount(700*4), core.amount(5900*4) ) ); + + // firstly it will match with buy_high, at buy_high's price + BOOST_CHECK( !db.find( buy_high ) ); + // buy_high pays 111 CORE, receives 10 USD goes to buyer3's balance + BOOST_CHECK_EQUAL( 10, get_balance(buyer3, bitusd) ); + BOOST_CHECK_EQUAL( init_balance - 111, get_balance(buyer3, core) ); + + // then it will match with call, at mssp: 1/11 = 1000/11000 + const call_order_object* tmp_call = db.find( call_id ); + BOOST_CHECK( tmp_call != nullptr ); + + // call will receive call_to_cover, pay 11*call_to_cover + share_type call_to_pay = call_to_cover * 11; + BOOST_CHECK_EQUAL( 1000 - call_to_cover.value, call.debt.value ); + BOOST_CHECK_EQUAL( 15000 - call_to_pay.value, call.collateral.value ); + // new collateral ratio should be higher than mcr as well as tcr + BOOST_CHECK( call.debt.value * 10 * 1750 < call.collateral.value * 1000 ); + idump( (call) ); + // borrower's balance doesn't change + BOOST_CHECK_EQUAL( init_balance - 15000, get_balance(borrower, core) ); + BOOST_CHECK_EQUAL( 0, get_balance(borrower, bitusd) ); + + // the limit order then will match with call2, at mssp: 1/11 = 1000/11000 + const call_order_object* tmp_call2 = db.find( call2_id ); + BOOST_CHECK( tmp_call2 != nullptr ); + + // call2 will receive call2_to_cover, pay 11*call2_to_cover + share_type call2_to_pay = call2_to_cover * 11; + BOOST_CHECK_EQUAL( 1000 - call2_to_cover.value, call2.debt.value ); + BOOST_CHECK_EQUAL( 15500 - call2_to_pay.value, call2.collateral.value ); + // new collateral ratio should be higher than mcr as well as tcr + BOOST_CHECK( call2.debt.value * 10 * 2000 < call2.collateral.value * 1000 ); + idump( (call2) ); + // borrower2's balance doesn't change + BOOST_CHECK_EQUAL( init_balance - 15500, get_balance(borrower2, core) ); + BOOST_CHECK_EQUAL( 0, get_balance(borrower2, bitusd) ); + + // then it will match with buy_med, at buy_med's price. Since buy_med is too big, it's partially filled. + // buy_med receives the remaining USD of sell order, minus market fees, goes to buyer2's balance + share_type buy_med_get = 700*4 - 10 - call_to_cover - call2_to_cover; + share_type buy_med_pay = buy_med_get * 11; // buy_med pays at 1/11 + buy_med_get -= (buy_med_get/100); // minus 1% market fee + BOOST_CHECK_EQUAL( buy_med_get.value, get_balance(buyer2, bitusd) ); + BOOST_CHECK_EQUAL( init_balance - 33000, get_balance(buyer2, core) ); + BOOST_CHECK_EQUAL( db.find( buy_med )->for_sale.value, 33000-buy_med_pay.value ); + + // call3 is not in margin call territory so won't be matched + BOOST_CHECK_EQUAL( 1000, call3.debt.value ); + BOOST_CHECK_EQUAL( 25000, call3.collateral.value ); + + // buy_low's price is too low that won't be matched + BOOST_CHECK_EQUAL( db.find( buy_low )->for_sale.value, 80 ); + + // check seller balance + BOOST_CHECK_EQUAL( 193, get_balance(seller, bitusd) ); // 3000 - 7 - 700*4 + BOOST_CHECK_EQUAL( 30801, get_balance(seller, core) ); // 111 + (700*4-10)*11 + + // Cancel buy_med + cancel_limit_order( buy_med(db) ); + BOOST_CHECK( !db.find( buy_med ) ); + BOOST_CHECK_EQUAL( buy_med_get.value, get_balance(buyer2, bitusd) ); + BOOST_CHECK_EQUAL( init_balance - buy_med_pay.value, get_balance(buyer2, core) ); + + // Create another sell order slightly below the call price, won't fill + limit_order_id_type sell_med = create_sell_order( seller, bitusd.amount(7), core.amount(59) )->id; + BOOST_CHECK_EQUAL( db.find( sell_med )->for_sale.value, 7 ); + // check seller balance + BOOST_CHECK_EQUAL( 193-7, get_balance(seller, bitusd) ); + BOOST_CHECK_EQUAL( 30801, get_balance(seller, core) ); + + // call3 is not in margin call territory so won't be matched + BOOST_CHECK_EQUAL( 1000, call3.debt.value ); + BOOST_CHECK_EQUAL( 25000, call3.collateral.value ); + + // buy_low's price is too low that won't be matched + BOOST_CHECK_EQUAL( db.find( buy_low )->for_sale.value, 80 ); + + // generate a block + generate_block(); + +} FC_LOG_AND_RETHROW() } + +/*** + * BSIP38 "target_collateral_ratio" test: matching a maker limit order with multiple taker call orders + */ +BOOST_AUTO_TEST_CASE(target_cr_test_call_limit) +{ try { + auto mi = db.get_global_properties().parameters.maintenance_interval; + generate_blocks(HARDFORK_CORE_834_TIME - mi); + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + + set_expiration( db, trx ); + + ACTORS((buyer)(seller)(borrower)(borrower2)(borrower3)(feedproducer)); + + const auto& bitusd = create_bitasset("USDBIT", feedproducer_id); + const auto& core = asset_id_type()(db); + + int64_t init_balance(1000000); + + transfer(committee_account, buyer_id, asset(init_balance)); + transfer(committee_account, borrower_id, asset(init_balance)); + transfer(committee_account, borrower2_id, asset(init_balance)); + transfer(committee_account, borrower3_id, asset(init_balance)); + update_feed_producers( bitusd, {feedproducer.id} ); + + price_feed current_feed; + current_feed.maintenance_collateral_ratio = 1750; + current_feed.maximum_short_squeeze_ratio = 1100; + current_feed.settlement_price = bitusd.amount( 1 ) / core.amount(5); + publish_feed( bitusd, feedproducer, current_feed ); + // start out with 300% collateral, call price is 15/1.75 CORE/USD = 60/7, tcr 170% is lower than 175% + const call_order_object& call = *borrow( borrower, bitusd.amount(1000), asset(15000), 1700); + call_order_id_type call_id = call.id; + // create another position with 310% collateral, call price is 15.5/1.75 CORE/USD = 62/7, tcr 200% is higher than 175% + const call_order_object& call2 = *borrow( borrower2, bitusd.amount(1000), asset(15500), 2000); + call_order_id_type call2_id = call2.id; + // create yet another position with 500% collateral, call price is 25/1.75 CORE/USD = 100/7, no tcr + const call_order_object& call3 = *borrow( borrower3, bitusd.amount(1000), asset(25000)); + transfer(borrower, seller, bitusd.amount(1000)); + transfer(borrower2, seller, bitusd.amount(1000)); + transfer(borrower3, seller, bitusd.amount(1000)); + + BOOST_CHECK_EQUAL( 1000, call.debt.value ); + BOOST_CHECK_EQUAL( 15000, call.collateral.value ); + BOOST_CHECK_EQUAL( 1000, call2.debt.value ); + BOOST_CHECK_EQUAL( 15500, call2.collateral.value ); + BOOST_CHECK_EQUAL( 1000, call3.debt.value ); + BOOST_CHECK_EQUAL( 25000, call3.collateral.value ); + BOOST_CHECK_EQUAL( 3000, get_balance(seller, bitusd) ); + BOOST_CHECK_EQUAL( 0, get_balance(seller, core) ); + BOOST_CHECK_EQUAL( 3000, get_balance(seller, bitusd) ); + BOOST_CHECK_EQUAL( init_balance - 15000, get_balance(borrower, core) ); + BOOST_CHECK_EQUAL( init_balance - 15500, get_balance(borrower2, core) ); + BOOST_CHECK_EQUAL( init_balance - 25000, get_balance(borrower3, core) ); + BOOST_CHECK_EQUAL( 0, get_balance(borrower, bitusd) ); + BOOST_CHECK_EQUAL( 0, get_balance(borrower2, bitusd) ); + BOOST_CHECK_EQUAL( 0, get_balance(borrower3, bitusd) ); + + // This sell order above MSSP will not be matched with a call + limit_order_id_type sell_high = create_sell_order(seller, bitusd.amount(7), core.amount(78))->id; + BOOST_CHECK_EQUAL( db.find( sell_high )->for_sale.value, 7 ); + + BOOST_CHECK_EQUAL( 2993, get_balance(seller, bitusd) ); + BOOST_CHECK_EQUAL( 0, get_balance(seller, core) ); + + // This buy order is too low will not be matched with a sell order + limit_order_id_type buy_low = create_sell_order(buyer, asset(80), bitusd.amount(10))->id; + + BOOST_CHECK_EQUAL( 0, get_balance(buyer, bitusd) ); + BOOST_CHECK_EQUAL( init_balance - 80, get_balance(buyer, core) ); + + // Create a sell order which will be matched with several call orders later, price 1/9 + limit_order_id_type sell_id = create_sell_order(seller, bitusd.amount(500), core.amount(4500) )->id; + BOOST_CHECK_EQUAL( db.find( sell_id )->for_sale.value, 500 ); + + // prepare price feed to get call and call2 (but not call3) into margin call territory + current_feed.settlement_price = bitusd.amount( 1 ) / core.amount(10); + + // call and call2's CR is quite high, and debt amount is quite a lot, assume neither of them will be completely filled + price match_price = sell_id(db).sell_price; + share_type call_to_cover = call_id(db).get_max_debt_to_cover(match_price,current_feed.settlement_price,1750); + share_type call2_to_cover = call2_id(db).get_max_debt_to_cover(match_price,current_feed.settlement_price,1750); + BOOST_CHECK_LT( call_to_cover.value, call_id(db).debt.value ); + BOOST_CHECK_LT( call2_to_cover.value, call2_id(db).debt.value ); + // even though call2 has a higher CR, since call's TCR is less than call2's TCR, so we expect call will cover less when called + BOOST_CHECK_LT( call_to_cover.value, call2_to_cover.value ); + + // adjust price feed to get call and call2 (but not call3) into margin call territory + publish_feed( bitusd, feedproducer, current_feed ); + // settlement price = 1/10, mssp = 1/11 + + // firstly the limit order will match with call, at limit order's price: 1/9 + const call_order_object* tmp_call = db.find( call_id ); + BOOST_CHECK( tmp_call != nullptr ); + + // call will receive call_to_cover, pay 9*call_to_cover + share_type call_to_pay = call_to_cover * 9; + BOOST_CHECK_EQUAL( 1000 - call_to_cover.value, call.debt.value ); + BOOST_CHECK_EQUAL( 15000 - call_to_pay.value, call.collateral.value ); + // new collateral ratio should be higher than mcr as well as tcr + BOOST_CHECK( call.debt.value * 10 * 1750 < call.collateral.value * 1000 ); + idump( (call) ); + // borrower's balance doesn't change + BOOST_CHECK_EQUAL( init_balance - 15000, get_balance(borrower, core) ); + BOOST_CHECK_EQUAL( 0, get_balance(borrower, bitusd) ); + + // the limit order then will match with call2, at limit order's price: 1/9 + const call_order_object* tmp_call2 = db.find( call2_id ); + BOOST_CHECK( tmp_call2 != nullptr ); + + // if the limit is big enough, call2 will receive call2_to_cover, pay 11*call2_to_cover + // however it's not the case, so call2 will receive less + call2_to_cover = 500 - call_to_cover; + share_type call2_to_pay = call2_to_cover * 9; + BOOST_CHECK_EQUAL( 1000 - call2_to_cover.value, call2.debt.value ); + BOOST_CHECK_EQUAL( 15500 - call2_to_pay.value, call2.collateral.value ); + idump( (call2) ); + // borrower2's balance doesn't change + BOOST_CHECK_EQUAL( init_balance - 15500, get_balance(borrower2, core) ); + BOOST_CHECK_EQUAL( 0, get_balance(borrower2, bitusd) ); + + // call3 is not in margin call territory so won't be matched + BOOST_CHECK_EQUAL( 1000, call3.debt.value ); + BOOST_CHECK_EQUAL( 25000, call3.collateral.value ); + + // sell_id is completely filled + BOOST_CHECK( !db.find( sell_id ) ); + + // check seller balance + BOOST_CHECK_EQUAL( 2493, get_balance(seller, bitusd) ); // 3000 - 7 - 500 + BOOST_CHECK_EQUAL( 4500, get_balance(seller, core) ); // 500*9 + + // buy_low's price is too low that won't be matched + BOOST_CHECK_EQUAL( db.find( buy_low )->for_sale.value, 80 ); + + // generate a block + generate_block(); + +} FC_LOG_AND_RETHROW() } + BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/tests/operation_tests.cpp b/tests/tests/operation_tests.cpp index e9a643997f..476883a493 100644 --- a/tests/tests/operation_tests.cpp +++ b/tests/tests/operation_tests.cpp @@ -556,6 +556,127 @@ BOOST_AUTO_TEST_CASE( more_call_order_update_test_after_hardfork_583 ) } } +BOOST_AUTO_TEST_CASE( call_order_update_validation_test ) +{ + call_order_update_operation op; + + // throw on default values + BOOST_CHECK_THROW( op.validate(), fc::assert_exception ); + + // minimum changes to make it valid + op.delta_debt = asset( 1, asset_id_type(1) ); + op.validate(); // won't throw if has a non-zero debt with different asset_id_type than collateral + + // throw on negative fee + op.fee = asset( -1 ); + BOOST_CHECK_THROW( op.validate(), fc::assert_exception ); + op.fee = asset( 0 ); + + // throw on identical debt and collateral asset id + op.delta_collateral = asset( 0, asset_id_type(1) ); + BOOST_CHECK_THROW( op.validate(), fc::assert_exception ); + + // throw on zero debt and collateral amount + op.delta_debt = asset( 0, asset_id_type(0) ); + BOOST_CHECK_THROW( op.validate(), fc::assert_exception ); + op.delta_debt = asset( -1, asset_id_type(0) ); + + op.validate(); // valid now + + op.extensions.value.target_collateral_ratio = 0; + op.validate(); // still valid + + op.extensions.value.target_collateral_ratio = 65535; + op.validate(); // still valid + +} + +// Tests that target_cr option can't be set before hard fork core-834 +// TODO: remove this test case after hard fork +BOOST_AUTO_TEST_CASE( call_order_update_target_cr_hardfork_time_test ) +{ + try { + auto mi = db.get_global_properties().parameters.maintenance_interval; + generate_blocks(HARDFORK_CORE_834_TIME - mi); + + set_expiration( db, trx ); + + ACTORS((sam)(alice)(bob)); + const auto& bitusd = create_bitasset("USDBIT", sam.id); + const auto& core = asset_id_type()(db); + asset_id_type bitusd_id = bitusd.id; + asset_id_type core_id = core.id; + + transfer(committee_account, sam_id, asset(10000000)); + transfer(committee_account, alice_id, asset(10000000)); + transfer(committee_account, bob_id, asset(10000000)); + update_feed_producers( bitusd, {sam.id} ); + + price_feed current_feed; current_feed.settlement_price = bitusd.amount( 100 ) / core.amount(100); + current_feed.maintenance_collateral_ratio = 1750; // need to set this explicitly, testnet has a different default + current_feed.maximum_short_squeeze_ratio = 1100; // need to set this explicitly, testnet has a different default + publish_feed( bitusd, sam, current_feed ); + + FC_ASSERT( bitusd.bitasset_data(db).current_feed.settlement_price == current_feed.settlement_price ); + + BOOST_TEST_MESSAGE( "alice tries to borrow using 4x collateral at 1:1 price with target_cr set, " + "will fail before hard fork time" ); + GRAPHENE_REQUIRE_THROW( borrow( alice, bitusd.amount(100000), core.amount(400000), 0 ), fc::assert_exception ); + GRAPHENE_REQUIRE_THROW( borrow( alice, bitusd.amount(100000), core.amount(400000), 1 ), fc::assert_exception ); + GRAPHENE_REQUIRE_THROW( borrow( alice, bitusd.amount(100000), core.amount(400000), 1749 ), fc::assert_exception ); + GRAPHENE_REQUIRE_THROW( borrow( alice, bitusd.amount(100000), core.amount(400000), 1750 ), fc::assert_exception ); + GRAPHENE_REQUIRE_THROW( borrow( alice, bitusd.amount(100000), core.amount(400000), 1751 ), fc::assert_exception ); + GRAPHENE_REQUIRE_THROW( borrow( alice, bitusd.amount(100000), core.amount(400000), 65535 ), fc::assert_exception ); + + auto call_update_proposal = [this]( const account_object& proposer, + const account_object& updater, + const asset& delta_collateral, + const asset& delta_debt, + const optional target_cr ) + { + call_order_update_operation op; + op.funding_account = updater.id; + op.delta_collateral = delta_collateral; + op.delta_debt = delta_debt; + op.extensions.value.target_collateral_ratio = target_cr; + + const auto& curfees = *db.get_global_properties().parameters.current_fees; + const auto& proposal_create_fees = curfees.get(); + proposal_create_operation prop; + prop.fee_paying_account = proposer.id; + prop.proposed_ops.emplace_back( op ); + prop.expiration_time = db.head_block_time() + fc::days(1); + prop.fee = asset( proposal_create_fees.fee + proposal_create_fees.price_per_kbyte ); + + signed_transaction tx; + tx.operations.push_back( prop ); + db.current_fee_schedule().set_fee( tx.operations.back() ); + set_expiration( db, tx ); + db.push_transaction( tx, ~0 ); + }; + + BOOST_TEST_MESSAGE( "bob tries to propose a proposal with target_cr set, " + "will fail before hard fork time" ); + GRAPHENE_REQUIRE_THROW( call_update_proposal( bob, alice, bitusd.amount(10), core.amount(40), 0 ), fc::assert_exception ); + GRAPHENE_REQUIRE_THROW( call_update_proposal( bob, alice, bitusd.amount(10), core.amount(40), 1750 ), fc::assert_exception ); + GRAPHENE_REQUIRE_THROW( call_update_proposal( bob, alice, bitusd.amount(10), core.amount(40), 65535 ), fc::assert_exception ); + + generate_blocks( db.get_dynamic_global_properties().next_maintenance_time ); + set_expiration( db, trx ); + + BOOST_TEST_MESSAGE( "bob tries to propose a proposal with target_cr set, " + "will success after hard fork time" ); + // now able to propose + call_update_proposal( bob_id(db), alice_id(db), bitusd_id(db).amount(10), core_id(db).amount(40), 65535 ); + + generate_block(); + + } catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + /** * This test sets up a situation where a margin call will be executed and ensures that * it is properly filled.