Skip to content

Commit

Permalink
Merge pull request #2716 from bitshares/pr-2587-settle-without-feed
Browse files Browse the repository at this point in the history
Allow settling more than total debt amount in individual settlement fund when no sufficient price feeds
  • Loading branch information
abitmore authored Jan 23, 2023
2 parents 996bb3d + ff39fb5 commit e4878a3
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 2 deletions.
8 changes: 6 additions & 2 deletions libraries/chain/asset_evaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1341,13 +1341,16 @@ operation_result asset_settle_evaluator::do_apply(const asset_settle_evaluator::

// Process the rest
const auto& head_time = d.head_block_time();
const auto& maint_time = d.get_dynamic_global_properties().next_maintenance_time;
d.adjust_balance( op.account, -to_settle );

bool after_core_hardfork_2582 = HARDFORK_CORE_2582_PASSED( head_time ); // Price feed issues
if( after_core_hardfork_2582 && 0 == to_settle.amount )
return result;

bool after_core_hardfork_2587 = HARDFORK_CORE_2587_PASSED( head_time );
if( after_core_hardfork_2587 && bitasset.current_feed.settlement_price.is_null() )
return result;

d.adjust_balance( op.account, -to_settle );
const auto& settle = d.create<force_settlement_object>(
[&op,&to_settle,&head_time,&bitasset](force_settlement_object& s) {
s.owner = op.account;
Expand All @@ -1357,6 +1360,7 @@ operation_result asset_settle_evaluator::do_apply(const asset_settle_evaluator::

result.value.new_objects = flat_set<object_id_type>({ settle.id });

const auto& maint_time = d.get_dynamic_global_properties().next_maintenance_time;
if( HARDFORK_CORE_2481_PASSED( maint_time ) )
{
d.apply_force_settlement( settle, bitasset, *asset_to_settle );
Expand Down
6 changes: 6 additions & 0 deletions libraries/chain/hardfork.d/CORE_2587.hf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// bitshares-core issue #2587 settle more than total debt in individual settlement fund when no sufficient price feeds
#ifndef HARDFORK_CORE_2587_TIME
// Jan 1 2030, midnight; this is a dummy date until a hardfork date is scheduled
#define HARDFORK_CORE_2587_TIME (fc::time_point_sec( 1893456000 ))
#define HARDFORK_CORE_2587_PASSED(head_block_time) (head_block_time >= HARDFORK_CORE_2587_TIME)
#endif
202 changes: 202 additions & 0 deletions tests/tests/bsrm_indvd_settlement_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,208 @@ BOOST_AUTO_TEST_CASE( individual_settlement_to_fund_and_disable_force_settle_tes

} FC_CAPTURE_AND_RETHROW() }

/// Tests individual settlement to fund : if there is no sufficient price feeds,
/// * before core-2587 hard fork, cannot settle an amount more than the fund,
/// * after core-2587 hard fork, can settle an amount more than the fund: only pay from the fund, no settle order.
BOOST_AUTO_TEST_CASE( individual_settlement_to_fund_and_no_feed )
{ try {

// Advance to core-2467 hard fork
auto mi = db.get_global_properties().parameters.maintenance_interval;
generate_blocks(HARDFORK_CORE_2467_TIME - mi);
generate_blocks(db.get_dynamic_global_properties().next_maintenance_time);

{
set_expiration( db, trx );

ACTORS((sam)(feeder)(borrower)(borrower2)(seller));

auto init_amount = 10000000 * GRAPHENE_BLOCKCHAIN_PRECISION;
fund( sam, asset(init_amount) );
fund( feeder, asset(init_amount) );

using bsrm_type = bitasset_options::black_swan_response_type;
uint8_t bsrm_value = static_cast<uint8_t>(bsrm_type::individual_settlement_to_fund);

// Create asset
asset_id_type samcoin_id = create_user_issued_asset( "SAMCOIN", sam_id(db), charge_market_fee,
price(asset(1, asset_id_type(1)), asset(1)),
2, 100 ).get_id(); // fee 1%
issue_uia( borrower, asset(init_amount, samcoin_id) );
issue_uia( borrower2, asset(init_amount, samcoin_id) );

asset_create_operation acop;
acop.issuer = sam_id;
acop.symbol = "SAMMPA";
acop.precision = 2;
acop.common_options.core_exchange_rate = price(asset(1,asset_id_type(1)),asset(1));
acop.common_options.max_supply = GRAPHENE_MAX_SHARE_SUPPLY;
acop.common_options.market_fee_percent = 100; // 1%
acop.common_options.flags = charge_market_fee;
acop.common_options.issuer_permissions = ASSET_ISSUER_PERMISSION_ENABLE_BITS_MASK;
acop.bitasset_opts = bitasset_options();
acop.bitasset_opts->minimum_feeds = 1;
acop.bitasset_opts->feed_lifetime_sec = 300;
acop.bitasset_opts->short_backing_asset = samcoin_id;
acop.bitasset_opts->extensions.value.black_swan_response_method = bsrm_value;
acop.bitasset_opts->extensions.value.margin_call_fee_ratio = 11;
acop.bitasset_opts->extensions.value.force_settle_fee_percent = 300;

trx.operations.clear();
trx.operations.push_back( acop );
processed_transaction ptx = PUSH_TX(db, trx, ~0);
const asset_object& mpa = db.get<asset_object>(ptx.operation_results[0].get<object_id_type>());
asset_id_type mpa_id = mpa.get_id();

BOOST_CHECK( mpa.bitasset_data(db).get_black_swan_response_method()
== bsrm_type::individual_settlement_to_fund );

// add a price feed publisher and publish a feed
update_feed_producers( mpa_id, { feeder_id } );

price_feed f;
f.settlement_price = price( asset(100,mpa_id), asset(1,samcoin_id) );
f.core_exchange_rate = price( asset(100,mpa_id), asset(1) );
f.maintenance_collateral_ratio = 1850;
f.maximum_short_squeeze_ratio = 1250;

uint16_t feed_icr = 1900;

publish_feed( mpa_id, feeder_id, f, feed_icr );

BOOST_CHECK( mpa_id(db).bitasset_data(db).median_feed.settlement_price == f.settlement_price );
BOOST_CHECK( mpa_id(db).bitasset_data(db).current_feed.settlement_price == f.settlement_price );
BOOST_CHECK( !mpa_id(db).bitasset_data(db).is_current_feed_price_capped() );
BOOST_CHECK( !mpa_id(db).bitasset_data(db).has_settlement() );
BOOST_CHECK( !mpa_id(db).bitasset_data(db).has_individual_settlement() );
BOOST_CHECK( !db.find_settled_debt_order(mpa_id) );

// borrowers borrow some
// undercollateralization price = 100000:2000 * 1250:1000 = 100000:1600
const call_order_object* call_ptr = borrow( borrower, asset(100000, mpa_id), asset(2000, samcoin_id) );
BOOST_REQUIRE( call_ptr );
call_order_id_type call_id = call_ptr->get_id();

// undercollateralization price = 100000:2500 * 1250:1000 = 100000:2000
const call_order_object* call2_ptr = borrow( borrower2, asset(100000, mpa_id), asset(2500, samcoin_id) );
BOOST_REQUIRE( call2_ptr );
call_order_id_type call2_id = call2_ptr->get_id();

// Transfer funds to sellers
transfer( borrower, seller, asset(100000,mpa_id) );
transfer( borrower2, seller, asset(100000,mpa_id) );

BOOST_CHECK_EQUAL( call_id(db).debt.value, 100000 );
BOOST_CHECK_EQUAL( call_id(db).collateral.value, 2000 );
BOOST_CHECK_EQUAL( call2_id(db).debt.value, 100000 );
BOOST_CHECK_EQUAL( call2_id(db).collateral.value, 2500 );

BOOST_CHECK_EQUAL( get_balance( seller_id, mpa_id ), 200000 );
BOOST_CHECK_EQUAL( get_balance( seller_id, samcoin_id ), 0 );

// publish a new feed so that borrower's debt position is undercollateralized
f.settlement_price = price( asset(100000,mpa_id), asset(1650,samcoin_id) );
publish_feed( mpa_id, feeder_id, f, feed_icr );
// call pays price = 100000:1650 * 1000:1250 = 100000:2062.5 = 48.484848485
// call match price = 100000:1650 * 1000:1239 = 100000:2048.75 = 48.915303153

// check
BOOST_CHECK( mpa_id(db).bitasset_data(db).median_feed.settlement_price == f.settlement_price );
BOOST_CHECK( !mpa_id(db).bitasset_data(db).has_settlement() );
BOOST_CHECK( mpa_id(db).bitasset_data(db).has_individual_settlement() );
BOOST_CHECK( !db.find_settled_debt_order(mpa_id) );

// call: margin call fee deducted = round_down(2000*11/1250) = 17,
// fund receives 2000 - 17 = 1983
BOOST_CHECK_EQUAL( mpa_id(db).bitasset_data(db).individual_settlement_fund.value, 1983 );
BOOST_CHECK_EQUAL( mpa_id(db).bitasset_data(db).individual_settlement_debt.value, 100000 );

BOOST_CHECK( mpa_id(db).bitasset_data(db).current_feed.settlement_price
== price( asset(100000*1239,mpa_id), asset(1983*1000,samcoin_id) ) );
BOOST_CHECK( mpa_id(db).bitasset_data(db).is_current_feed_price_capped() );

BOOST_CHECK( !db.find( call_id ) );
BOOST_CHECK_EQUAL( call2_id(db).debt.value, 100000 );
BOOST_CHECK_EQUAL( call2_id(db).collateral.value, 2500 );

// let the feed expire
{
generate_blocks( db.head_block_time() + fc::seconds(350) );
set_expiration( db, trx );

BOOST_CHECK( mpa_id(db).bitasset_data(db).median_feed.settlement_price.is_null() );
BOOST_CHECK( mpa_id(db).bitasset_data(db).current_feed.settlement_price.is_null() );

BOOST_CHECK( !mpa_id(db).bitasset_data(db).has_settlement() );
BOOST_CHECK( mpa_id(db).bitasset_data(db).has_individual_settlement() );
BOOST_CHECK( !db.find_settled_debt_order(mpa_id) );

BOOST_CHECK_EQUAL( mpa_id(db).bitasset_data(db).individual_settlement_fund.value, 1983 );
BOOST_CHECK_EQUAL( mpa_id(db).bitasset_data(db).individual_settlement_debt.value, 100000 );

BOOST_CHECK( !db.find( call_id ) );
BOOST_CHECK_EQUAL( call2_id(db).debt.value, 100000 );
BOOST_CHECK_EQUAL( call2_id(db).collateral.value, 2500 );
}

// Before core-2587 hard fork, unable to settle more than the fund when no feed
BOOST_CHECK_THROW( force_settle( seller, asset(100001,mpa_id) ), fc::exception );

// Advance to core-2587 hard fork
generate_blocks(HARDFORK_CORE_2587_TIME - mi);
generate_blocks(db.get_dynamic_global_properties().next_maintenance_time);
set_expiration( db, trx );

// able to settle more than the fund
auto result = force_settle( seller_id(db), asset(100001,mpa_id) );
auto op_result = result.get<extendable_operation_result>().value;

auto check_result = [&]
{
// seller gets 1983, market fee 19, finally gets 1964
// seller pays 100000
BOOST_CHECK( !op_result.new_objects.valid() ); // no delayed force settlement
BOOST_REQUIRE( op_result.paid.valid() && 1U == op_result.paid->size() );
BOOST_CHECK( *op_result.paid->begin() == asset( 100000, mpa_id ) );
BOOST_REQUIRE( op_result.received.valid() && 1U == op_result.received->size() );
BOOST_CHECK( *op_result.received->begin() == asset( 1964, samcoin_id ) );
BOOST_REQUIRE( op_result.fees.valid() && 1U == op_result.fees->size() );
BOOST_CHECK( *op_result.fees->begin() == asset( 19, samcoin_id ) );
// fund is now empty

BOOST_CHECK_EQUAL( mpa_id(db).bitasset_data(db).individual_settlement_fund.value, 0 );
BOOST_CHECK_EQUAL( mpa_id(db).bitasset_data(db).individual_settlement_debt.value, 0 );

BOOST_CHECK( !mpa_id(db).bitasset_data(db).has_settlement() );
BOOST_CHECK( !mpa_id(db).bitasset_data(db).has_individual_settlement() );
BOOST_CHECK( !db.find_settled_debt_order(mpa_id) );

BOOST_CHECK( mpa_id(db).bitasset_data(db).median_feed.settlement_price.is_null() );
BOOST_CHECK( mpa_id(db).bitasset_data(db).current_feed.settlement_price.is_null() );
BOOST_CHECK( !mpa_id(db).bitasset_data(db).is_current_feed_price_capped() );

BOOST_CHECK( !db.find( call_id ) );
BOOST_CHECK_EQUAL( call2_id(db).debt.value, 100000 );
BOOST_CHECK_EQUAL( call2_id(db).collateral.value, 2500 );

BOOST_CHECK_EQUAL( get_balance( seller_id, mpa_id ), 100000 ); // 200000 - 100000
BOOST_CHECK_EQUAL( get_balance( seller_id, samcoin_id ), 1964 );

// Unable to settle when the fund is empty and no feed
BOOST_CHECK_THROW( force_settle( seller, asset(1000,mpa_id) ), fc::exception );
};

check_result();

BOOST_TEST_MESSAGE( "Generate a block" );
generate_block();

check_result();

}

} FC_CAPTURE_AND_RETHROW() }

/// Tests individual settlement to fund : settles when price drops, and how taker orders would match after that
BOOST_AUTO_TEST_CASE( individual_settlement_to_fund_and_taking_test )
{ try {
Expand Down

0 comments on commit e4878a3

Please sign in to comment.