diff --git a/libraries/chain/asset_evaluator.cpp b/libraries/chain/asset_evaluator.cpp index 0f32a9a32b..1f30290c87 100644 --- a/libraries/chain/asset_evaluator.cpp +++ b/libraries/chain/asset_evaluator.cpp @@ -56,6 +56,16 @@ namespace detail { } } + // TODO review and remove code below and links to it after HARDFORK_BSIP_77_TIME + void check_bitasset_options_hf_bsip77(const fc::time_point_sec& block_time, const bitasset_options& options) + { + if ( !HARDFORK_BSIP_77_PASSED( block_time ) ) { + // ICR should not be set until activation of BSIP77 + FC_ASSERT(!options.extensions.value.initial_collateral_ratio.valid(), + "Initial collateral ratio should not be defined before HARDFORK_BSIP_77_TIME"); + } + } + void check_asset_claim_fees_hardfork_87_74_collatfee(const fc::time_point_sec& block_time, const asset_claim_fees_operation& op) { // HF_REMOVABLE: Following hardfork check should be removable after hardfork date passes: @@ -70,12 +80,14 @@ void_result asset_create_evaluator::do_evaluate( const asset_create_operation& o { try { const database& d = db(); + // Define now from the current block time + const time_point_sec now = d.head_block_time(); const auto& chain_parameters = d.get_global_properties().parameters; FC_ASSERT( op.common_options.whitelist_authorities.size() <= chain_parameters.maximum_asset_whitelist_authorities ); FC_ASSERT( op.common_options.blacklist_authorities.size() <= chain_parameters.maximum_asset_whitelist_authorities ); - detail::check_asset_options_hf_1774(d.head_block_time(), op.common_options); + detail::check_asset_options_hf_1774( now, op.common_options ); // Check that all authorities do exist for( auto id : op.common_options.whitelist_authorities ) @@ -87,8 +99,6 @@ void_result asset_create_evaluator::do_evaluate( const asset_create_operation& o auto asset_symbol_itr = asset_indx.find( op.symbol ); FC_ASSERT( asset_symbol_itr == asset_indx.end() ); - // Define now from the current block time - const time_point_sec now = d.head_block_time(); // This must remain due to "BOND.CNY" being allowed before this HF if( now > HARDFORK_385_TIME ) { @@ -107,6 +117,7 @@ void_result asset_create_evaluator::do_evaluate( const asset_create_operation& o if( op.bitasset_opts ) { + detail::check_bitasset_options_hf_bsip77( now, *op.bitasset_opts ); const asset_object& backing = op.bitasset_opts->short_backing_asset(d); if( backing.is_market_issued() ) { @@ -306,7 +317,7 @@ void_result asset_update_evaluator::do_evaluate(const asset_update_operation& o) validate_new_issuer( d, a, *o.new_issuer ); } - detail::check_asset_options_hf_1774(d.head_block_time(), o.new_options); + detail::check_asset_options_hf_1774( now, o.new_options ); if( a.dynamic_asset_data_id(d).current_supply != 0 ) { @@ -445,6 +456,8 @@ void_result asset_update_bitasset_evaluator::do_evaluate(const asset_update_bita { try { database& d = db(); + detail::check_bitasset_options_hf_bsip77( d.head_block_time(), op.new_options ); + const asset_object& asset_obj = op.asset_to_update(d); FC_ASSERT( asset_obj.is_market_issued(), "Cannot update BitAsset-specific settings on a non-BitAsset." ); @@ -580,6 +593,12 @@ static bool update_bitasset_object_options( is_witness_or_committee_fed = true; } + // check if ICR will change + const auto& old_icr = bdo.options.extensions.value.initial_collateral_ratio; + const auto& new_icr = op.new_options.extensions.value.initial_collateral_ratio; + bool icr_changed = ( ( old_icr.valid() != new_icr.valid() ) + || ( old_icr.valid() && *old_icr != *new_icr ) ); + bdo.options = op.new_options; // are we modifying the underlying? If so, reset the feeds @@ -609,6 +628,11 @@ static bool update_bitasset_object_options( // We need to call check_call_orders if the settlement price changes after hardfork core-868-890 return ( after_hf_core_868_890 && ! (old_feed == bdo.current_feed) ); } + else if( icr_changed ) // feeds not updated, but ICR changed + { + // update data derived from ICR + bdo.refresh_current_initial_collateralization(); + } return false; } diff --git a/libraries/chain/asset_object.cpp b/libraries/chain/asset_object.cpp index ce74e934b4..e442465d36 100644 --- a/libraries/chain/asset_object.cpp +++ b/libraries/chain/asset_object.cpp @@ -69,7 +69,12 @@ void graphene::chain::asset_bitasset_data_object::update_median_feeds( time_poin current_feed_publication_time = current_time; current_feed = price_feed(); if( after_core_hardfork_1270 ) + { + // update data derived from MCR current_maintenance_collateralization = price(); + // update data derived from ICR + current_initial_collateralization = price(); + } return; } if( current_feeds.size() == 1 ) @@ -79,7 +84,12 @@ void graphene::chain::asset_bitasset_data_object::update_median_feeds( time_poin current_feed = current_feeds.front(); // Note: perhaps can defer updating current_maintenance_collateralization for better performance if( after_core_hardfork_1270 ) + { + // update data derived from MCR current_maintenance_collateralization = current_feed.maintenance_collateralization(); + // update data derived from ICR + refresh_current_initial_collateralization(); + } return; } @@ -102,10 +112,27 @@ void graphene::chain::asset_bitasset_data_object::update_median_feeds( time_poin current_feed = median_feed; // Note: perhaps can defer updating current_maintenance_collateralization for better performance if( after_core_hardfork_1270 ) + { + // update data derived from MCR current_maintenance_collateralization = current_feed.maintenance_collateralization(); + // update data derived from ICR + refresh_current_initial_collateralization(); + } } - +void asset_bitasset_data_object::refresh_current_initial_collateralization() +{ + if( current_feed.settlement_price.is_null() ) + current_initial_collateralization = price(); + else + { + const auto& icr = options.extensions.value.initial_collateral_ratio; + if( icr.valid() && *icr > current_feed.maintenance_collateral_ratio ) // if ICR is set and is above MCR + current_initial_collateralization = current_feed.calculate_initial_collateralization( *icr ); + else // if ICR is not set, or not above MCR + current_initial_collateralization = current_maintenance_collateralization; + } +} asset asset_object::amount_from_string(string amount_string) const { try { @@ -186,6 +213,7 @@ FC_REFLECT_DERIVED_NO_TYPENAME( graphene::chain::asset_bitasset_data_object, (gr (current_feed) (current_feed_publication_time) (current_maintenance_collateralization) + (current_initial_collateralization) (options) (force_settled_volume) (is_prediction_market) diff --git a/libraries/chain/hardfork.d/BSIP_77.hf b/libraries/chain/hardfork.d/BSIP_77.hf new file mode 100644 index 0000000000..f476e5999f --- /dev/null +++ b/libraries/chain/hardfork.d/BSIP_77.hf @@ -0,0 +1,6 @@ +// BSIP 77 ("Initial Collateral Ratio" (ICR)) hardfork check +#ifndef HARDFORK_BSIP_77_TIME +// Jan 1 2030, midnight; this is a dummy date until a hardfork date is scheduled +#define HARDFORK_BSIP_77_TIME (fc::time_point_sec( 1893456000 )) +#define HARDFORK_BSIP_77_PASSED(now) (now >= HARDFORK_BSIP_77_TIME) +#endif diff --git a/libraries/chain/include/graphene/chain/asset_object.hpp b/libraries/chain/include/graphene/chain/asset_object.hpp index 4e6fccee83..97255a9610 100644 --- a/libraries/chain/include/graphene/chain/asset_object.hpp +++ b/libraries/chain/include/graphene/chain/asset_object.hpp @@ -237,6 +237,15 @@ namespace graphene { namespace chain { /// Call orders with collateralization (aka collateral/debt) not greater than this value are in margin call territory. /// This value is derived from @ref current_feed for better performance and should be kept consistent. price current_maintenance_collateralization; + /// After BSIP77, when creating a new debt position or updating an existing position, the position + /// will be checked against the `initial_collateral_ratio` (ICR) parameter in the bitasset options. + /// This value is derived from @ref current_feed and `ICR` for better performance and should be kept + /// consistent. + price current_initial_collateralization; + + /// Derive @ref current_initial_collateralization from other member variables. + /// Note: this assumes @ref current_maintenance_collateralization is fresh. + void refresh_current_initial_collateralization(); /// True if this asset implements a @ref prediction_market bool is_prediction_market = false; diff --git a/libraries/chain/market_evaluator.cpp b/libraries/chain/market_evaluator.cpp index 10a02125a4..960072d0b2 100644 --- a/libraries/chain/market_evaluator.cpp +++ b/libraries/chain/market_evaluator.cpp @@ -354,7 +354,8 @@ object_id_type call_order_update_evaluator::do_apply(const call_order_update_ope ("a", ~call_obj->call_price )("b", _bitasset_data->current_feed.settlement_price) ); } - else // after hard fork, always allow call order to be updated if collateral ratio is increased and debt is not increased + else // after hard fork core-583, always allow call order to be updated if collateral ratio + // is increased and debt is not increased { // We didn't fill any call orders. This may be because we // aren't in margin call territory, or it may be because there @@ -362,9 +363,15 @@ object_id_type call_order_update_evaluator::do_apply(const call_order_update_ope // if collateral ratio is not increased or debt is increased, we throw. // be here, we know no margin call was executed, // so call_obj's collateral ratio should be set only by op + // ------ + // Before BSIP77, CR of the new/updated position is required to be above MCR; + // after BSIP77, CR of the new/updated position is required to be above max(ICR,MCR). + // The `current_initial_collateralization` variable has been initialized according to the logic, + // so we directly use it here. FC_ASSERT( ( !before_core_hardfork_1270 - && call_obj->collateralization() > _bitasset_data->current_maintenance_collateralization ) - || ( before_core_hardfork_1270 && ~call_obj->call_price < _bitasset_data->current_feed.settlement_price ) + && call_obj->collateralization() > _bitasset_data->current_initial_collateralization ) + || ( before_core_hardfork_1270 + && ~call_obj->call_price < _bitasset_data->current_feed.settlement_price ) || ( old_collateralization.valid() && call_obj->debt <= *old_debt && call_obj->collateralization() > *old_collateralization ), "Can only increase collateral ratio without increasing debt if would trigger a margin call that " diff --git a/libraries/chain/proposal_evaluator.cpp b/libraries/chain/proposal_evaluator.cpp index d5a244787b..544e07ef3e 100644 --- a/libraries/chain/proposal_evaluator.cpp +++ b/libraries/chain/proposal_evaluator.cpp @@ -29,8 +29,9 @@ namespace graphene { namespace chain { namespace detail { - void check_asset_options_hf_1774(const fc::time_point_sec& block_time, const asset_options& options); + void check_asset_options_hf_1774(const fc::time_point_sec& block_time, const asset_options& options); void check_asset_options_hf_bsip81(const fc::time_point_sec& block_time, const asset_options& options); + void check_bitasset_options_hf_bsip77(const fc::time_point_sec& block_time, const bitasset_options& options); void check_asset_claim_fees_hardfork_87_74_collatfee(const fc::time_point_sec& block_time, const asset_claim_fees_operation& op); } @@ -51,6 +52,10 @@ struct proposal_operation_hardfork_visitor // hf_1774 detail::check_asset_options_hf_1774(block_time, v.common_options); + // HARDFORK_BSIP_77 + if( v.bitasset_opts.valid() ) + detail::check_bitasset_options_hf_bsip77( block_time, *v.bitasset_opts ); + // HARDFORK_BSIP_81 detail::check_asset_options_hf_bsip81(block_time, v.common_options); } @@ -61,6 +66,10 @@ struct proposal_operation_hardfork_visitor // HARDFORK_BSIP_81 detail::check_asset_options_hf_bsip81(block_time, v.new_options); } + void operator()(const graphene::chain::asset_update_bitasset_operation &v) const { + // HARDFORK_BSIP_77 + detail::check_bitasset_options_hf_bsip77( block_time, v.new_options ); + } void operator()(const graphene::chain::asset_claim_fees_operation &v) const { detail::check_asset_claim_fees_hardfork_87_74_collatfee(block_time, v); // HF_REMOVABLE diff --git a/libraries/protocol/asset.cpp b/libraries/protocol/asset.cpp index 71e7665563..8588d91c3a 100644 --- a/libraries/protocol/asset.cpp +++ b/libraries/protocol/asset.cpp @@ -294,6 +294,13 @@ namespace graphene { namespace protocol { return ~settlement_price * ratio_type( maintenance_collateral_ratio, GRAPHENE_COLLATERAL_RATIO_DENOM ); } + price price_feed::calculate_initial_collateralization( uint16_t initial_collateral_ratio )const + { + if( settlement_price.is_null() ) + return price(); + return ~settlement_price * ratio_type( initial_collateral_ratio, GRAPHENE_COLLATERAL_RATIO_DENOM ); + } + // compile-time table of powers of 10 using template metaprogramming template< int N > diff --git a/libraries/protocol/asset_ops.cpp b/libraries/protocol/asset_ops.cpp index 8ba6aa2b26..7bfd65ef00 100644 --- a/libraries/protocol/asset_ops.cpp +++ b/libraries/protocol/asset_ops.cpp @@ -210,6 +210,12 @@ void bitasset_options::validate() const FC_ASSERT(minimum_feeds > 0); FC_ASSERT(force_settlement_offset_percent <= GRAPHENE_100_PERCENT); FC_ASSERT(maximum_force_settlement_volume <= GRAPHENE_100_PERCENT); + + if( extensions.value.initial_collateral_ratio.valid() ) + { + FC_ASSERT( *extensions.value.initial_collateral_ratio >= GRAPHENE_MIN_COLLATERAL_RATIO ); + FC_ASSERT( *extensions.value.initial_collateral_ratio <= GRAPHENE_MAX_COLLATERAL_RATIO ); + } } void asset_options::validate()const @@ -265,6 +271,7 @@ void asset_claim_pool_operation::validate()const { } } // namespace graphene::protocol GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::asset_options ) +GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::bitasset_options::ext ) GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::bitasset_options ) GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::additional_asset_options ) GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::asset_create_operation::fee_parameters_type ) diff --git a/libraries/protocol/include/graphene/protocol/asset.hpp b/libraries/protocol/include/graphene/protocol/asset.hpp index 9b0b9fea48..674b130ec1 100644 --- a/libraries/protocol/include/graphene/protocol/asset.hpp +++ b/libraries/protocol/include/graphene/protocol/asset.hpp @@ -206,6 +206,9 @@ namespace graphene { namespace protocol { /// Calculation: ~settlement_price * maintenance_collateral_ratio / GRAPHENE_COLLATERAL_RATIO_DENOM price maintenance_collateralization()const; + /// The result will be used to check new debt positions and position updates. + /// Calculation: ~settlement_price * initial_collateral_ratio / GRAPHENE_COLLATERAL_RATIO_DENOM + price calculate_initial_collateralization( uint16_t initial_collateral_ratio )const; ///@} friend bool operator == ( const price_feed& a, const price_feed& b ) diff --git a/libraries/protocol/include/graphene/protocol/asset_ops.hpp b/libraries/protocol/include/graphene/protocol/asset_ops.hpp index 9aa410d4ca..cc24c17f6e 100644 --- a/libraries/protocol/include/graphene/protocol/asset_ops.hpp +++ b/libraries/protocol/include/graphene/protocol/asset_ops.hpp @@ -100,6 +100,15 @@ namespace graphene { namespace protocol { * @note Changes to this struct will break protocol compatibility */ struct bitasset_options { + + struct ext + { + /// After BSIP77, when creating a new debt position or updating an existing position, + /// the position will be checked against this parameter. + /// Unused for prediction markets, although we allow it to be set for simpler implementation + fc::optional initial_collateral_ratio; + }; + /// Time before a price feed expires uint32_t feed_lifetime_sec = GRAPHENE_DEFAULT_PRICE_FEED_LIFETIME; /// Minimum number of unexpired feeds required to extract a median feed from @@ -117,7 +126,8 @@ namespace graphene { namespace protocol { /// This speicifies which asset type is used to collateralize short sales /// This field may only be updated if the current supply of the asset is zero. asset_id_type short_backing_asset; - extensions_type extensions; + + extension extensions; /// Perform internal consistency checks. /// @throws fc::exception if any check fails @@ -551,6 +561,9 @@ FC_REFLECT( graphene::protocol::asset_options, (description) (extensions) ) + +FC_REFLECT( graphene::protocol::bitasset_options::ext, (initial_collateral_ratio) ) + FC_REFLECT( graphene::protocol::bitasset_options, (feed_lifetime_sec) (minimum_feeds) @@ -561,8 +574,11 @@ FC_REFLECT( graphene::protocol::bitasset_options, (extensions) ) -FC_REFLECT( graphene::protocol::additional_asset_options, (reward_percent)(whitelist_market_fee_sharing)(taker_fee_percent) ) -FC_REFLECT( graphene::protocol::asset_create_operation::fee_parameters_type, (symbol3)(symbol4)(long_symbol)(price_per_kbyte) ) +FC_REFLECT( graphene::protocol::additional_asset_options, + (reward_percent)(whitelist_market_fee_sharing)(taker_fee_percent) ) + +FC_REFLECT( graphene::protocol::asset_create_operation::fee_parameters_type, + (symbol3)(symbol4)(long_symbol)(price_per_kbyte) ) FC_REFLECT( graphene::protocol::asset_global_settle_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::protocol::asset_settle_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::protocol::asset_settle_cancel_operation::fee_parameters_type, ) @@ -624,6 +640,7 @@ FC_REFLECT( graphene::protocol::asset_reserve_operation, FC_REFLECT( graphene::protocol::asset_fund_fee_pool_operation, (fee)(from_account)(asset_id)(amount)(extensions) ); GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::asset_options ) +GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::bitasset_options::ext ) GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::bitasset_options ) GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::additional_asset_options ) GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::asset_create_operation::fee_parameters_type ) diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index 9c1354b06c..0958e5e331 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -434,6 +434,7 @@ void database_fixture::vote_for_committee_and_witnesses(uint16_t num_committee, op.fee = db.current_fee_schedule().calculate_fee( op ); + trx.operations.clear(); trx.operations.push_back(op); trx.validate(); PUSH_TX(db, trx, ~0); @@ -697,16 +698,17 @@ const account_object& database_fixture::get_account( const string& name )const return *itr; } -const asset_object& database_fixture::create_bitasset( +asset_create_operation database_fixture::make_bitasset( const string& name, account_id_type issuer /* = GRAPHENE_WITNESS_ACCOUNT */, uint16_t market_fee_percent /* = 100 */ /* 1% */, uint16_t flags /* = charge_market_fee */, uint16_t precision /* = GRAPHENE_BLOCKCHAIN_PRECISION_DIGITS */, asset_id_type backing_asset /* = CORE */, - share_type max_supply /* = GRAPHENE_MAX_SHARE_SUPPLY */ + share_type max_supply, /* = GRAPHENE_MAX_SHARE_SUPPLY */ + optional initial_cr /* = {} */ ) -{ try { +{ asset_create_operation creator; creator.issuer = issuer; creator.fee = asset(); @@ -721,6 +723,24 @@ const asset_object& database_fixture::create_bitasset( creator.common_options.core_exchange_rate = price(asset(1,asset_id_type(1)),asset(1)); creator.bitasset_opts = bitasset_options(); creator.bitasset_opts->short_backing_asset = backing_asset; + creator.bitasset_opts->extensions.value.initial_collateral_ratio = initial_cr; + return creator; +} + +const asset_object& database_fixture::create_bitasset( + const string& name, + account_id_type issuer /* = GRAPHENE_WITNESS_ACCOUNT */, + uint16_t market_fee_percent /* = 100 */ /* 1% */, + uint16_t flags /* = charge_market_fee */, + uint16_t precision /* = GRAPHENE_BLOCKCHAIN_PRECISION_DIGITS */, + asset_id_type backing_asset /* = CORE */, + share_type max_supply, /* = GRAPHENE_MAX_SHARE_SUPPLY */ + optional initial_cr /* = {} */ + ) +{ try { + asset_create_operation creator = make_bitasset( name, issuer, market_fee_percent, flags, + precision, backing_asset, max_supply, initial_cr ); + trx.operations.clear(); trx.operations.push_back(std::move(creator)); trx.validate(); processed_transaction ptx = PUSH_TX(db, trx, ~0); @@ -752,6 +772,7 @@ const asset_object& database_fixture::create_prediction_market( creator.bitasset_opts = bitasset_options(); creator.bitasset_opts->short_backing_asset = backing_asset; creator.is_prediction_market = true; + trx.operations.clear(); trx.operations.push_back(std::move(creator)); trx.validate(); processed_transaction ptx = PUSH_TX(db, trx, ~0); @@ -772,6 +793,7 @@ const asset_object& database_fixture::create_user_issued_asset( const string& na creator.common_options.max_supply = GRAPHENE_MAX_SHARE_SUPPLY; creator.common_options.flags = charge_market_fee; creator.common_options.issuer_permissions = charge_market_fee; + trx.operations.clear(); trx.operations.push_back(std::move(creator)); trx.validate(); processed_transaction ptx = PUSH_TX(db, trx, ~0); @@ -812,6 +834,7 @@ void database_fixture::issue_uia( const account_object& recipient, asset amount op.issuer = amount.asset_id(db).issuer; op.asset_to_issue = amount; op.issue_to_account = recipient.id; + trx.operations.clear(); trx.operations.push_back(op); PUSH_TX( db, trx, ~0 ); trx.operations.clear(); @@ -857,6 +880,7 @@ const account_object& database_fixture::create_account( const public_key_type& key /* = public_key_type() */ ) { + trx.operations.clear(); trx.operations.push_back(make_account(name, key)); trx.validate(); processed_transaction ptx = PUSH_TX(db, trx, ~0); @@ -924,6 +948,7 @@ const committee_member_object& database_fixture::create_committee_member( const { committee_member_create_operation op; op.committee_member_account = owner.id; + trx.operations.clear(); trx.operations.push_back(op); trx.validate(); processed_transaction ptx = PUSH_TX(db, trx, ~0); @@ -945,6 +970,7 @@ const witness_object& database_fixture::create_witness( const account_object& ow witness_create_operation op; op.witness_account = owner.id; op.block_signing_key = signing_private_key.get_public_key(); + trx.operations.clear(); trx.operations.push_back(op); trx.validate(); processed_transaction ptx = PUSH_TX(db, trx, skip_flags ); @@ -960,6 +986,7 @@ const worker_object& database_fixture::create_worker( const account_id_type owne op.initializer = burn_worker_initializer(); op.work_begin_date = db.head_block_time(); op.work_end_date = op.work_begin_date + duration; + trx.operations.clear(); trx.operations.push_back(op); trx.validate(); processed_transaction ptx = PUSH_TX(db, trx, ~0); @@ -1021,6 +1048,7 @@ asset database_fixture::cancel_limit_order( const limit_order_object& order ) limit_order_cancel_operation cancel_order; cancel_order.fee_paying_account = order.seller; cancel_order.order = order.id; + trx.operations.clear(); trx.operations.push_back(cancel_order); for( auto& op : trx.operations ) db.current_fee_schedule().set_fee(op); trx.validate(); @@ -1053,6 +1081,7 @@ void database_fixture::transfer( trans.from = from.id; trans.to = to.id; trans.amount = amount; + trx.operations.clear(); trx.operations.push_back(trans); if( fee == asset() ) @@ -1129,6 +1158,7 @@ void database_fixture::publish_feed(const account_id_type& publisher, op.asset_id = asset2; op.feed.settlement_price = ~price(a1.amount(amount1),a2.amount(amount2)); op.feed.core_exchange_rate = ~price(core.amount(amount1), a2.amount(amount2)); + trx.operations.clear(); trx.operations.push_back(std::move(op)); PUSH_TX( db, trx, ~0); generate_block(); @@ -1233,6 +1263,7 @@ void database_fixture::fund_fee_pool( const account_object& from, const asset_ob fund.from_account = from.id; fund.asset_id = asset_to_fund.id; fund.amount = amount; + trx.operations.clear(); trx.operations.push_back( fund ); for( auto& op : trx.operations ) db.current_fee_schedule().set_fee(op); @@ -1508,6 +1539,7 @@ void database_fixture::set_htlc_committee_parameters() uop.new_parameters.current_fees = new_fee_schedule; cop.proposed_ops.emplace_back(uop); + trx.operations.clear(); trx.operations.push_back(cop); graphene::chain::processed_transaction proc_trx = db.push_transaction(trx); trx.clear(); diff --git a/tests/common/database_fixture.hpp b/tests/common/database_fixture.hpp index a3cc5624df..df153fb195 100644 --- a/tests/common/database_fixture.hpp +++ b/tests/common/database_fixture.hpp @@ -199,8 +199,9 @@ struct database_fixture { bool skip_key_index_test = false; uint32_t anon_acct_count; bool hf1270 = false; + bool bsip77 = false; - database_fixture(const fc::time_point_sec &initial_timestamp = + database_fixture(const fc::time_point_sec &initial_timestamp = fc::time_point_sec(GRAPHENE_TESTING_GENESIS_TIMESTAMP)); ~database_fixture(); @@ -282,13 +283,22 @@ struct database_fixture { const asset_object& get_asset( const string& symbol )const; const account_object& get_account( const string& name )const; + asset_create_operation make_bitasset( const string& name, + account_id_type issuer = GRAPHENE_WITNESS_ACCOUNT, + uint16_t market_fee_percent = 100 /*1%*/, + uint16_t flags = charge_market_fee, + uint16_t precision = 2, + asset_id_type backing_asset = {}, + share_type max_supply = GRAPHENE_MAX_SHARE_SUPPLY, + optional initial_cr = {} ); const asset_object& create_bitasset(const string& name, account_id_type issuer = GRAPHENE_WITNESS_ACCOUNT, uint16_t market_fee_percent = 100 /*1%*/, uint16_t flags = charge_market_fee, uint16_t precision = 2, asset_id_type backing_asset = {}, - share_type max_supply = GRAPHENE_MAX_SHARE_SUPPLY ); + share_type max_supply = GRAPHENE_MAX_SHARE_SUPPLY, + optional initial_cr = {} ); const asset_object& create_prediction_market(const string& name, account_id_type issuer = GRAPHENE_WITNESS_ACCOUNT, uint16_t market_fee_percent = 100 /*1%*/, diff --git a/tests/tests/bitasset_tests.cpp b/tests/tests/bitasset_tests.cpp index 0c58af042d..797e3a2e66 100644 --- a/tests/tests/bitasset_tests.cpp +++ b/tests/tests/bitasset_tests.cpp @@ -927,7 +927,7 @@ BOOST_AUTO_TEST_CASE( hf_1270_test ) generate_blocks( db.get_dynamic_global_properties().next_maintenance_time, true, skip ); generate_block( skip ); - for( int i = 0; i < 8; ++i ) + for( int i = 0; i < 10; ++i ) { idump( (i) ); int blocks = 0; @@ -943,6 +943,10 @@ BOOST_AUTO_TEST_CASE( hf_1270_test ) generate_blocks( HARDFORK_CORE_1270_TIME - mi, true, skip ); generate_blocks( db.get_dynamic_global_properties().next_maintenance_time, true, skip ); } + else if( i == 8 ) // go beyond hard fork BSIP77 + { + generate_blocks( HARDFORK_BSIP_77_TIME, true, skip ); + } set_expiration( db, trx ); ACTORS( (seller)(borrower)(feedproducer)(feedproducer2)(feedproducer3) ); diff --git a/tests/tests/operation_tests.cpp b/tests/tests/operation_tests.cpp index ecf906b6a9..b4fd6c89c9 100644 --- a/tests/tests/operation_tests.cpp +++ b/tests/tests/operation_tests.cpp @@ -184,7 +184,10 @@ BOOST_AUTO_TEST_CASE( old_call_order_update_test_after_hardfork_583 ) { try { - generate_blocks( HARDFORK_CORE_583_TIME ); + auto hf_time = HARDFORK_CORE_583_TIME; + if( bsip77 ) + hf_time = HARDFORK_BSIP_77_TIME; + generate_blocks( hf_time ); generate_block(); set_expiration( db, trx ); @@ -340,6 +343,204 @@ BOOST_AUTO_TEST_CASE( asset_settle_cancel_operation_test_after_hf588 ) } } +/// Test case for bsip77: +/// * the "initial_collateral_ratio" parameter can only be set after the BSIP77 hard fork +/// * the parameter should be within a range +// TODO removed the hard fork part after the hard fork, keep the valid range part +BOOST_AUTO_TEST_CASE( bsip77_hardfork_time_and_param_valid_range_test ) +{ + try { + + // Proceeds to a recent hard fork + generate_blocks( HARDFORK_CORE_583_TIME ); + generate_block(); + set_expiration( db, trx ); + + ACTORS((sam)); + + // Before bsip77 hard fork, unable to create a bitasset with ICR + BOOST_CHECK_THROW( create_bitasset( "USDBIT", sam_id, 100, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY, 0 ), fc::exception ); + BOOST_CHECK_THROW( create_bitasset( "USDBIT", sam_id, 100, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY, 1 ), fc::exception ); + BOOST_CHECK_THROW( create_bitasset( "USDBIT", sam_id, 100, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY, 1000 ), fc::exception ); + BOOST_CHECK_THROW( create_bitasset( "USDBIT", sam_id, 100, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY, 1001 ), fc::exception ); + BOOST_CHECK_THROW( create_bitasset( "USDBIT", sam_id, 100, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY, 1750 ), fc::exception ); + BOOST_CHECK_THROW( create_bitasset( "USDBIT", sam_id, 100, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY, 32000 ), fc::exception ); + BOOST_CHECK_THROW( create_bitasset( "USDBIT", sam_id, 100, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY, 32001 ), fc::exception ); + + // Can create a bitasset without ICR + const auto& bitusd = create_bitasset( "USDBIT", sam.id, 100, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY ); + asset_id_type usd_id = bitusd.id; + + // helper function for setting ICR for an asset + auto set_icr_for_asset = [&](asset_id_type aid, optional icr) { + const asset_object& ao = aid(db); + const asset_bitasset_data_object& abo = ao.bitasset_data(db); + asset_update_bitasset_operation uop; + uop.issuer = ao.issuer; + uop.asset_to_update = aid; + uop.new_options = abo.options; + uop.new_options.extensions.value.initial_collateral_ratio = icr; + trx.operations.clear(); + trx.operations.push_back( uop ); + trx.validate(); + set_expiration( db, trx ); + PUSH_TX(db, trx, ~0); + }; + + // Before bsip77 hard fork, unable to update a bitasset with ICR + BOOST_CHECK_THROW( set_icr_for_asset( usd_id, 0 ), fc::exception ); + BOOST_CHECK_THROW( set_icr_for_asset( usd_id, 1 ), fc::exception ); + BOOST_CHECK_THROW( set_icr_for_asset( usd_id, 1000 ), fc::exception ); + BOOST_CHECK_THROW( set_icr_for_asset( usd_id, 1001 ), fc::exception ); + BOOST_CHECK_THROW( set_icr_for_asset( usd_id, 1750 ), fc::exception ); + BOOST_CHECK_THROW( set_icr_for_asset( usd_id, 32000 ), fc::exception ); + BOOST_CHECK_THROW( set_icr_for_asset( usd_id, 32001 ), fc::exception ); + + // helper function for creating a proposal which contains an asset_create_operation with ICR + auto propose_create_bitasset = [&]( string name, optional icr ) { + asset_create_operation acop = make_bitasset( name, sam_id, 100, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY, icr ); + proposal_create_operation cop; + cop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + cop.expiration_time = db.head_block_time() + 100; + cop.proposed_ops.emplace_back( acop ); + trx.operations.clear(); + trx.operations.push_back( cop ); + trx.validate(); + set_expiration( db, trx ); + processed_transaction ptx = PUSH_TX(db, trx, ~0); + trx.operations.clear(); + }; + + // Before bsip77 hard fork, unable to create a proposal with an asset_create_operation with ICR + BOOST_CHECK_THROW( propose_create_bitasset( "USDBITA", 0 ), fc::exception ); + BOOST_CHECK_THROW( propose_create_bitasset( "USDBITA", 1 ), fc::exception ); + BOOST_CHECK_THROW( propose_create_bitasset( "USDBITA", 1000 ), fc::exception ); + BOOST_CHECK_THROW( propose_create_bitasset( "USDBITA", 1001 ), fc::exception ); + BOOST_CHECK_THROW( propose_create_bitasset( "USDBITA", 1750 ), fc::exception ); + BOOST_CHECK_THROW( propose_create_bitasset( "USDBITA", 32000 ), fc::exception ); + BOOST_CHECK_THROW( propose_create_bitasset( "USDBITA", 32001 ), fc::exception ); + + // helper function for creating a proposal which contains an asset_update_bitasset_operation with ICR + auto propose_set_icr_for_asset = [&](asset_id_type aid, optional icr) { + const asset_object& ao = aid(db); + const asset_bitasset_data_object& abo = ao.bitasset_data(db); + asset_update_bitasset_operation uop; + uop.issuer = ao.issuer; + uop.asset_to_update = aid; + uop.new_options = abo.options; + uop.new_options.extensions.value.initial_collateral_ratio = icr; + + proposal_create_operation cop; + cop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + cop.expiration_time = db.head_block_time() + 100; + cop.proposed_ops.emplace_back( uop ); + trx.operations.clear(); + trx.operations.push_back( cop ); + trx.validate(); + set_expiration( db, trx ); + PUSH_TX(db, trx, ~0); + trx.operations.clear(); + }; + + // Before bsip77 hard fork, unable to create a proposal with an asset_update_bitasset_op with ICR + BOOST_CHECK_THROW( propose_set_icr_for_asset( usd_id, 0 ), fc::exception ); + BOOST_CHECK_THROW( propose_set_icr_for_asset( usd_id, 1 ), fc::exception ); + BOOST_CHECK_THROW( propose_set_icr_for_asset( usd_id, 1000 ), fc::exception ); + BOOST_CHECK_THROW( propose_set_icr_for_asset( usd_id, 1001 ), fc::exception ); + BOOST_CHECK_THROW( propose_set_icr_for_asset( usd_id, 1750 ), fc::exception ); + BOOST_CHECK_THROW( propose_set_icr_for_asset( usd_id, 32000 ), fc::exception ); + BOOST_CHECK_THROW( propose_set_icr_for_asset( usd_id, 32001 ), fc::exception ); + + // Pass the hard fork time + generate_blocks( HARDFORK_BSIP_77_TIME ); + set_expiration( db, trx ); + + // Unable to create a bitasset with an invalid ICR + BOOST_CHECK_THROW( create_bitasset( "USDBITB", sam_id, 0, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY, 0 ), fc::exception ); + BOOST_CHECK_THROW( create_bitasset( "USDBITB", sam_id, 1, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY, 0 ), fc::exception ); + BOOST_CHECK_THROW( create_bitasset( "USDBITB", sam_id, 1000, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY, 0 ), fc::exception ); + BOOST_CHECK_THROW( create_bitasset( "USDBITB", sam_id, 32001, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY, 0 ), fc::exception ); + // Able to create a bitasset with a valid ICR + asset_id_type usdc_id = create_bitasset( "USDBITC", sam.id, 100, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY, 1001 ).id; + asset_id_type usdd_id = create_bitasset( "USDBITD", sam.id, 100, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY, 1750 ).id; + asset_id_type usde_id = create_bitasset( "USDBITE", sam.id, 100, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY, 32000 ).id; + // Able to create a bitasset without ICR + asset_id_type usdf_id = create_bitasset( "USDBITF", sam.id, 100, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY, {} ).id; + + BOOST_CHECK( usdc_id(db).bitasset_data(db).options.extensions.value.initial_collateral_ratio == 1001 ); + BOOST_CHECK( usdd_id(db).bitasset_data(db).options.extensions.value.initial_collateral_ratio == 1750 ); + BOOST_CHECK( usde_id(db).bitasset_data(db).options.extensions.value.initial_collateral_ratio == 32000 ); + BOOST_CHECK( !usdf_id(db).bitasset_data(db).options.extensions.value.initial_collateral_ratio.valid() ); + + // Unable to update a bitasset with an invalid ICR + BOOST_CHECK_THROW( set_icr_for_asset( usd_id, 0 ), fc::exception ); + BOOST_CHECK_THROW( set_icr_for_asset( usd_id, 1 ), fc::exception ); + BOOST_CHECK_THROW( set_icr_for_asset( usd_id, 1000 ), fc::exception ); + BOOST_CHECK_THROW( set_icr_for_asset( usd_id, 32001 ), fc::exception ); + // Able to update a bitasset with a valid ICR + set_icr_for_asset( usd_id, 1001 ); + BOOST_CHECK( usd_id(db).bitasset_data(db).options.extensions.value.initial_collateral_ratio == 1001 ); + set_icr_for_asset( usd_id, 1750 ); + BOOST_CHECK( usd_id(db).bitasset_data(db).options.extensions.value.initial_collateral_ratio == 1750 ); + set_icr_for_asset( usd_id, 32000 ); + BOOST_CHECK( usd_id(db).bitasset_data(db).options.extensions.value.initial_collateral_ratio == 32000 ); + // Able to update a bitasset, unset its ICR + set_icr_for_asset( usd_id, {} ); + BOOST_CHECK( !usd_id(db).bitasset_data(db).options.extensions.value.initial_collateral_ratio.valid() ); + + // Unable to create a proposal with an asset_create_operation with an invalid ICR + BOOST_CHECK_THROW( propose_create_bitasset( "USDBITG", 0 ), fc::exception ); + BOOST_CHECK_THROW( propose_create_bitasset( "USDBITG", 1 ), fc::exception ); + BOOST_CHECK_THROW( propose_create_bitasset( "USDBITG", 1000 ), fc::exception ); + BOOST_CHECK_THROW( propose_create_bitasset( "USDBITG", 32001 ), fc::exception ); + // able to create a proposal with a valid ICR or no ICR + propose_create_bitasset( "USDBITG", 1001 ); + propose_create_bitasset( "USDBITG", 1750 ); + propose_create_bitasset( "USDBITG", 32000 ); + propose_create_bitasset( "USDBITG", {} ); + + // Unable to create a proposal with an asset_update_bitasset_op with an invalid ICR + BOOST_CHECK_THROW( propose_set_icr_for_asset( usd_id, 0 ), fc::exception ); + BOOST_CHECK_THROW( propose_set_icr_for_asset( usd_id, 1 ), fc::exception ); + BOOST_CHECK_THROW( propose_set_icr_for_asset( usd_id, 1000 ), fc::exception ); + BOOST_CHECK_THROW( propose_set_icr_for_asset( usd_id, 32001 ), fc::exception ); + // Able to create a proposal with a valid ICR or no ICR + propose_set_icr_for_asset( usd_id, 1001 ); + propose_set_icr_for_asset( usd_id, 1750 ); + propose_set_icr_for_asset( usd_id, 32000 ); + propose_set_icr_for_asset( usd_id, {} ); + + generate_block(); + + } catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + +BOOST_AUTO_TEST_CASE( old_call_order_update_test_after_hardfork_bsip77_when_icr_not_set ) +{ + bsip77 = true; + INVOKE( old_call_order_update_test_after_hardfork_583 ); +} + BOOST_AUTO_TEST_CASE( more_call_order_update_test ) { try { @@ -447,7 +648,10 @@ BOOST_AUTO_TEST_CASE( more_call_order_update_test_after_hardfork_583 ) { try { - generate_blocks( HARDFORK_CORE_583_TIME ); + auto hf_time = HARDFORK_CORE_583_TIME; + if( bsip77 ) + hf_time = HARDFORK_BSIP_77_TIME; + generate_blocks( hf_time ); generate_block(); set_expiration( db, trx ); @@ -556,53 +760,45 @@ BOOST_AUTO_TEST_CASE( more_call_order_update_test_after_hardfork_583 ) } } -BOOST_AUTO_TEST_CASE( call_order_update_validation_test ) +BOOST_AUTO_TEST_CASE( more_call_order_update_test_after_hardfork_bsip77_when_icr_not_set ) { - 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 + bsip77 = true; + INVOKE( more_call_order_update_test_after_hardfork_583 ); } -// 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 ) +BOOST_AUTO_TEST_CASE( more_call_order_update_test_after_hardfork_bsip77_when_icr_is_set ) { try { + + auto hf_time = HARDFORK_BSIP_77_TIME; + generate_blocks( hf_time ); + generate_block(); set_expiration( db, trx ); - ACTORS((sam)(alice)(bob)); - const auto& bitusd = create_bitasset("USDBIT", sam.id); + ACTORS((dan)(sam)(alice)(bob)); + const auto& bitusd = create_bitasset( "USDBIT", sam.id, 100, charge_market_fee, 2, {}, + GRAPHENE_MAX_SHARE_SUPPLY, 1050 ); // ICR = 1.05 const auto& core = asset_id_type()(db); - asset_id_type bitusd_id = bitusd.id; - asset_id_type core_id = core.id; + asset_id_type usd_id = bitusd.id; + + // helper function for setting ICR for an asset + auto set_icr_for_asset = [&](asset_id_type aid, optional icr) { + const asset_object& ao = aid(db); + const asset_bitasset_data_object& abo = ao.bitasset_data(db); + asset_update_bitasset_operation uop; + uop.issuer = ao.issuer; + uop.asset_to_update = aid; + uop.new_options = abo.options; + uop.new_options.extensions.value.initial_collateral_ratio = icr; + trx.operations.clear(); + trx.operations.push_back( uop ); + trx.validate(); + set_expiration( db, trx ); + PUSH_TX(db, trx, ~0); + }; + + transfer(committee_account, dan_id, asset(10000000)); transfer(committee_account, sam_id, asset(10000000)); transfer(committee_account, alice_id, asset(10000000)); transfer(committee_account, bob_id, asset(10000000)); @@ -615,40 +811,153 @@ BOOST_AUTO_TEST_CASE( call_order_update_target_cr_hardfork_time_test ) FC_ASSERT( bitusd.bitasset_data(db).current_feed.settlement_price == current_feed.settlement_price ); - 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; + BOOST_TEST_MESSAGE( "ICR 1.05, MCR 1.75" ); + BOOST_TEST_MESSAGE( "attempting to borrow using <=1.75x collateral at 1:1 price should not be allowed" ); + GRAPHENE_REQUIRE_THROW( borrow( bob, bitusd.amount(10000), core.amount(17499) ), fc::exception ); + GRAPHENE_REQUIRE_THROW( borrow( bob, bitusd.amount(10000), core.amount(17500) ), fc::exception ); - const auto& curfees = db.get_global_properties().parameters.get_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 ); + BOOST_TEST_MESSAGE( "alice borrow using 1.7501x collateral at 1:1 price should be allowed" ); + BOOST_CHECK( borrow( alice, bitusd.amount(10000), core.amount(17501) ) != nullptr ); + BOOST_REQUIRE_EQUAL( get_balance( alice, bitusd ), 10000 ); + BOOST_REQUIRE_EQUAL( get_balance( alice, core ), 10000000 - 17501 ); + BOOST_TEST_MESSAGE( "ICR 1.05, MCR 1.75, Alice CR 1.7501" ); + + // Update ICR + BOOST_TEST_MESSAGE( "Updating ICR to 1.85" ); + set_icr_for_asset( usd_id, 1850 ); + BOOST_TEST_MESSAGE( "ICR 1.85, MCR 1.75, Alice CR 1.7501" ); + + BOOST_TEST_MESSAGE( "alice adding more collateral should be allowed" ); + BOOST_CHECK( borrow( alice, bitusd.amount(0), core.amount(18000-17501) ) != nullptr ); + BOOST_REQUIRE_EQUAL( get_balance( alice, bitusd ), 10000 ); + BOOST_REQUIRE_EQUAL( get_balance( alice, core ), 10000000 - 18000 ); + BOOST_TEST_MESSAGE( "ICR 1.85, MCR 1.75, Alice CR 1.8000" ); + + BOOST_TEST_MESSAGE( "alice reducing collateral should not be allowed if CR<=1.85 and not margin called" ); + GRAPHENE_REQUIRE_THROW( cover( alice, bitusd.amount(0), core.amount(1) ), fc::exception ); + + BOOST_TEST_MESSAGE( "alice borrow using 1.8502x collateral at 1:1 price should be allowed" ); + BOOST_CHECK( borrow( alice, bitusd.amount(0), core.amount(18502-18000) ) != nullptr ); + BOOST_REQUIRE_EQUAL( get_balance( alice, bitusd ), 10000 ); + BOOST_REQUIRE_EQUAL( get_balance( alice, core ), 10000000 - 18502 ); + BOOST_TEST_MESSAGE( "ICR 1.85, MCR 1.75, Alice CR 1.8502" ); + + BOOST_TEST_MESSAGE( "alice reducing collateral to >1.85x should be allowed" ); + cover( alice, bitusd.amount(0), core.amount(1) ); + BOOST_REQUIRE_EQUAL( get_balance( alice, bitusd ), 10000 ); + BOOST_REQUIRE_EQUAL( get_balance( alice, core ), 10000000 - 18501 ); + BOOST_TEST_MESSAGE( "ICR 1.85, MCR 1.75, Alice CR 1.8501" ); + + BOOST_TEST_MESSAGE( "alice reducing collateral to <=1.85x should not be allowed if not margin called" ); + GRAPHENE_REQUIRE_THROW( cover( alice, bitusd.amount(0), core.amount(1) ), fc::exception ); - signed_transaction tx; - tx.operations.push_back( prop ); - db.current_fee_schedule().set_fee( tx.operations.back() ); - set_expiration( db, tx ); - PUSH_TX( db, tx, ~0 ); - }; + BOOST_TEST_MESSAGE( "alice borrow using 4x collateral at 1:1 price" ); + BOOST_CHECK( borrow( alice, bitusd.amount(100000-10000), core.amount(400000-18501) ) != nullptr ); + BOOST_REQUIRE_EQUAL( get_balance( alice, bitusd ), 100000 ); + BOOST_REQUIRE_EQUAL( get_balance( alice, core ), 10000000 - 400000 ); + BOOST_TEST_MESSAGE( "ICR 1.85, MCR 1.75, Alice CR 4.0000" ); - generate_blocks(HARDFORK_CORE_834_TIME); - set_expiration( db, trx ); + BOOST_TEST_MESSAGE( "alice place an order to sell usd at 1.05" ); + const limit_order_id_type alice_sell_id = create_sell_order( alice, bitusd.amount(1000), core.amount(1050) )->id; + BOOST_REQUIRE_EQUAL( get_balance( alice, bitusd ), 100000 - 1000 ); + BOOST_REQUIRE_EQUAL( get_balance( alice, core ), 10000000 - 400000 ); + + BOOST_TEST_MESSAGE( "bob attempting to borrow too much using 1.75x collateral at 1:1 price should not be allowed" ); + GRAPHENE_REQUIRE_THROW( borrow( bob, bitusd.amount(10000), core.amount(17500) ), fc::exception ); - 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 ); + BOOST_TEST_MESSAGE( "bob attempting to borrow less using 1.75x collateral at 1:1 price should be allowed and margin called" ); + BOOST_CHECK( !borrow( bob, bitusd.amount(100), core.amount(175) ) ); + BOOST_REQUIRE_EQUAL( get_balance( bob, bitusd ), 100 ); + BOOST_REQUIRE_EQUAL( get_balance( bob, core ), 10000000 - 105 ); + BOOST_REQUIRE_EQUAL( get_balance( alice, bitusd ), 100000 - 1000 ); + BOOST_REQUIRE_EQUAL( get_balance( alice, core ), 10000000 - 400000 + 105 ); + + BOOST_TEST_MESSAGE( "bob attempting to borrow using 2x collateral at 1:1 price now that there is a valid order" ); + const call_order_id_type bob_call_id = borrow( bob, bitusd.amount(100), asset(200))->id; + BOOST_REQUIRE_EQUAL( get_balance( bob, bitusd ), 100 + 100 ); + BOOST_REQUIRE_EQUAL( get_balance( bob, core ), 10000000 - 105 - 200 ); + + BOOST_TEST_MESSAGE( "bob attempting to borrow too much more using 1.75x collateral at 1:1 price should not be allowed" ); + GRAPHENE_REQUIRE_THROW( borrow( bob, bitusd.amount(10000-100), core.amount(17500-200) ), fc::exception ); + + BOOST_TEST_MESSAGE( "bob attempting to reduce collateral to 1.75x at 1:1 price should be allowed and margin called" ); + BOOST_CHECK( !borrow( bob, bitusd.amount(0), core.amount(175-200) ) ); + BOOST_REQUIRE_EQUAL( get_balance( bob, bitusd ), 100 + 100 ); + BOOST_REQUIRE_EQUAL( get_balance( bob, core ), 10000000 - 105 - 105 ); + BOOST_REQUIRE_EQUAL( get_balance( alice, bitusd ), 100000 - 1000 ); + BOOST_REQUIRE_EQUAL( get_balance( alice, core ), 10000000 - 400000 + 105 + 105 ); + BOOST_CHECK( !db.find( bob_call_id ) ); + + BOOST_TEST_MESSAGE( "alice cancel sell order" ); + cancel_limit_order( alice_sell_id(db) ); + + BOOST_TEST_MESSAGE( "dan attempting to borrow using 2x collateral at 1:1 price now that there is a valid order" ); + borrow( dan, bitusd.amount(5000), asset(10000)); + BOOST_REQUIRE_EQUAL( get_balance( dan, bitusd ), 5000 ); + BOOST_REQUIRE_EQUAL( get_balance( dan, core ), 10000000 - 10000 ); + + BOOST_TEST_MESSAGE( "sam update price feed so dan's position will enter margin call territory." ); + current_feed.settlement_price = bitusd.amount( 100 ) / core.amount(180); + publish_feed( bitusd, sam, current_feed ); + + BOOST_TEST_MESSAGE( "dan covering 2500 usd and freeing 5000 core should not be allowed..." ); + GRAPHENE_REQUIRE_THROW( cover( dan, bitusd.amount(2500), core.amount(5000) ), fc::exception ); + + BOOST_TEST_MESSAGE( "dan covering 2500 usd and freeing 5001 core should not be allowed..." ); + GRAPHENE_REQUIRE_THROW( cover( dan, bitusd.amount(2500), core.amount(5001) ), fc::exception ); + + BOOST_TEST_MESSAGE( "dan borrow 2500 more usd wth 5000 more core should not be allowed..." ); + GRAPHENE_REQUIRE_THROW( borrow( dan, bitusd.amount(2500), core.amount(5000) ), fc::exception ); + + BOOST_TEST_MESSAGE( "dan borrow 2500 more usd wth 4999 more core should not be allowed..." ); + GRAPHENE_REQUIRE_THROW( borrow( dan, bitusd.amount(2500), core.amount(4999) ), fc::exception ); + + BOOST_TEST_MESSAGE( "dan covering 2500 usd and freeing 4999 core should be allowed..." ); + cover( dan, bitusd.amount(2500), asset(4999)); + BOOST_REQUIRE_EQUAL( get_balance( dan, bitusd ), 2500 ); + BOOST_REQUIRE_EQUAL( get_balance( dan, core ), 10000000 - 10000 + 4999 ); + + BOOST_TEST_MESSAGE( "dan covering 0 usd and freeing 1 core should not be allowed..." ); + GRAPHENE_REQUIRE_THROW( cover( dan, bitusd.amount(0), core.amount(1) ), fc::exception ); + + BOOST_TEST_MESSAGE( "dan adding 1 core as collateral should be allowed..." ); + borrow( dan, bitusd.amount(0), asset(1)); + BOOST_REQUIRE_EQUAL( get_balance( dan, bitusd ), 2500 ); + BOOST_REQUIRE_EQUAL( get_balance( dan, core ), 10000000 - 10000 + 4999 - 1 ); + + BOOST_TEST_MESSAGE( "dan borrow 2500 more usd wth 5002 more core should not be allowed..." ); + GRAPHENE_REQUIRE_THROW( borrow( dan, bitusd.amount(2500), core.amount(5002) ), fc::exception ); + + BOOST_TEST_MESSAGE( "dan borrow 2500 more usd wth 5003 more core should not be allowed..." ); + GRAPHENE_REQUIRE_THROW( borrow( dan, bitusd.amount(2500), asset(5003) ), fc::exception ); + + // CR of Alice's postion is now 4.0 / 1.8 ~= 2.2222 + BOOST_TEST_MESSAGE( "ICR 1.85, MCR 1.75, Alice CR 2.222222" ); + + BOOST_TEST_MESSAGE( "alice adding more collateral should be allowed" ); + const call_order_id_type alice_call_id = borrow( alice, bitusd.amount(0), asset(1))->id; + BOOST_CHECK_EQUAL( alice_call_id(db).collateral.value, 400000 + 1 ); + BOOST_CHECK_EQUAL( alice_call_id(db).debt.value, 100000 ); + BOOST_TEST_MESSAGE( "ICR 1.85, MCR 1.75, Alice CR 2.222228" ); + + BOOST_TEST_MESSAGE( "alice reducing collateral to >1.85x should be allowed" ); + cover( alice, bitusd.amount(0), core.amount(67000) ); + BOOST_CHECK_EQUAL( alice_call_id(db).collateral.value, 333001 ); + BOOST_CHECK_EQUAL( alice_call_id(db).debt.value, 100000 ); + BOOST_TEST_MESSAGE( "ICR 1.85, MCR 1.75, Alice CR 1.850006" ); + + BOOST_TEST_MESSAGE( "alice reducing collateral to <=1.85x should not be allowed if not margin called" ); + GRAPHENE_REQUIRE_THROW( cover( alice, bitusd.amount(0), core.amount(1) ), fc::exception ); + + // Update ICR + BOOST_TEST_MESSAGE( "Updating ICR to 1.84" ); + set_icr_for_asset( usd_id, 1840 ); + BOOST_TEST_MESSAGE( "ICR 1.84, MCR 1.75, Alice CR 1.850006" ); + + BOOST_TEST_MESSAGE( "alice reducing collateral to >1.84x should be allowed" ); + cover( alice, bitusd.amount(0), core.amount(1) ); + BOOST_CHECK_EQUAL( alice_call_id(db).collateral.value, 333000 ); + BOOST_CHECK_EQUAL( alice_call_id(db).debt.value, 100000 ); generate_block(); @@ -658,6 +967,40 @@ BOOST_AUTO_TEST_CASE( call_order_update_target_cr_hardfork_time_test ) } } +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 +} + /** * This test sets up a situation where a margin call will be executed and ensures that * it is properly filled.