fixes bts mid 2018
This commit is contained in:
parent
ae712122d4
commit
4d8f3725b0
22 changed files with 950 additions and 222 deletions
|
|
@ -489,7 +489,7 @@ void_result asset_update_evaluator::do_apply(const asset_update_operation& o)
|
|||
for( auto itr = idx.lower_bound(o.asset_to_update);
|
||||
itr != idx.end() && itr->settlement_asset_id() == o.asset_to_update;
|
||||
itr = idx.lower_bound(o.asset_to_update) )
|
||||
d.cancel_order(*itr);
|
||||
d.cancel_settle_order(*itr);
|
||||
}
|
||||
|
||||
// For market-issued assets, if core change rate changed, update flag in bitasset data
|
||||
|
|
@ -683,6 +683,13 @@ void_result asset_global_settle_evaluator::do_evaluate(const asset_global_settle
|
|||
FC_ASSERT(asset_to_settle->can_global_settle());
|
||||
FC_ASSERT(asset_to_settle->issuer == op.issuer );
|
||||
FC_ASSERT(asset_to_settle->dynamic_data(d).current_supply > 0);
|
||||
|
||||
const asset_bitasset_data_object* bitasset_data = &asset_to_settle->bitasset_data(d);
|
||||
if( bitasset.is_prediction_market ) {
|
||||
/// if there is a settlement for this asset, then no further global settle may be taken and
|
||||
FC_ASSERT( !bitasset_data->has_settlement(),"This asset has settlement, cannot global settle twice" );
|
||||
}
|
||||
|
||||
const auto& idx = d.get_index_type<call_order_index>().indices().get<by_collateral>();
|
||||
assert( !idx.empty() );
|
||||
auto itr = idx.lower_bound(boost::make_tuple(price::min(asset_to_settle->bitasset_data(d).options.short_backing_asset,
|
||||
|
|
|
|||
|
|
@ -1939,6 +1939,40 @@ void database::perform_son_tasks()
|
|||
}
|
||||
}
|
||||
|
||||
void update_and_match_call_orders( database& db )
|
||||
{
|
||||
// Update call_price
|
||||
wlog( "Updating all call orders for hardfork core-343 at block ${n}", ("n",db.head_block_num()) );
|
||||
asset_id_type current_asset;
|
||||
const asset_bitasset_data_object* abd = nullptr;
|
||||
// by_collateral index won't change after call_price updated, so it's safe to iterate
|
||||
for( const auto& call_obj : db.get_index_type<call_order_index>().indices().get<by_collateral>() )
|
||||
{
|
||||
if( current_asset != call_obj.debt_type() ) // debt type won't be asset_id_type(), abd will always get initialized
|
||||
{
|
||||
current_asset = call_obj.debt_type();
|
||||
abd = ¤t_asset(db).bitasset_data(db);
|
||||
}
|
||||
if( !abd || abd->is_prediction_market ) // nothing to do with PM's; check !abd just to be safe
|
||||
continue;
|
||||
db.modify( call_obj, [&]( call_order_object& call ) {
|
||||
call.call_price = price::call_price( call.get_debt(), call.get_collateral(),
|
||||
abd->current_feed.maintenance_collateral_ratio );
|
||||
});
|
||||
}
|
||||
// Match call orders
|
||||
const auto& asset_idx = db.get_index_type<asset_index>().indices().get<by_type>();
|
||||
auto itr = asset_idx.lower_bound( true /** market issued */ );
|
||||
while( itr != asset_idx.end() )
|
||||
{
|
||||
const asset_object& a = *itr;
|
||||
++itr;
|
||||
// be here, next_maintenance_time should have been updated already
|
||||
db.check_call_orders( a, true, false ); // allow black swan, and call orders are taker
|
||||
}
|
||||
wlog( "Done updating all call orders for hardfork core-343 at block ${n}", ("n",db.head_block_num()) );
|
||||
}
|
||||
|
||||
void database::perform_chain_maintenance(const signed_block& next_block, const global_property_object& global_props)
|
||||
{ try {
|
||||
const auto& gpo = get_global_properties();
|
||||
|
|
@ -2195,11 +2229,20 @@ void database::perform_chain_maintenance(const signed_block& next_block, const g
|
|||
if( (dgpo.next_maintenance_time < HARDFORK_613_TIME) && (next_maintenance_time >= HARDFORK_613_TIME) )
|
||||
deprecate_annual_members(*this);
|
||||
|
||||
// To reset call_price of all call orders, then match by new rule
|
||||
bool to_update_and_match_call_orders = false;
|
||||
if( (dgpo.next_maintenance_time <= HARDFORK_CORE_343_TIME) && (next_maintenance_time > HARDFORK_CORE_343_TIME) )
|
||||
to_update_and_match_call_orders = true;
|
||||
|
||||
modify(dgpo, [next_maintenance_time](dynamic_global_property_object& d) {
|
||||
d.next_maintenance_time = next_maintenance_time;
|
||||
d.accounts_registered_this_interval = 0;
|
||||
});
|
||||
|
||||
// We need to do it after updated next_maintenance_time, to apply new rules here
|
||||
if( to_update_and_match_call_orders )
|
||||
update_and_match_call_orders(*this);
|
||||
|
||||
// Reset all BitAsset force settlement volumes to zero
|
||||
//for( const asset_bitasset_data_object* d : get_index_type<asset_bitasset_data_index>() )
|
||||
for( const auto& d : get_index_type<asset_bitasset_data_index>().indices() )
|
||||
|
|
|
|||
|
|
@ -66,15 +66,24 @@ void database::globally_settle_asset( const asset_object& mia, const price& sett
|
|||
auto call_end = call_price_index.upper_bound( price::max( bitasset.options.short_backing_asset, mia.id ) );
|
||||
while( call_itr != call_end )
|
||||
{
|
||||
auto pays = call_itr->get_debt() * settlement_price;
|
||||
auto pays = call_itr->get_debt() * settlement_price; // round down, in favor of call order
|
||||
|
||||
if( pays > call_itr->get_collateral() )
|
||||
pays = call_itr->get_collateral();
|
||||
|
||||
// Be here, the call order can be paying nothing, in this case, we take at least 1 Satoshi from call order
|
||||
if( pays.amount == 0 && !bitasset.is_prediction_market )
|
||||
{
|
||||
if( get_dynamic_global_properties().next_maintenance_time > HARDFORK_CORE_184_TIME )
|
||||
pays.amount = 1;
|
||||
else // TODO remove this warning after hard fork core-184
|
||||
wlog( "Something for nothing issue (#184, variant E) occurred at block #${block}", ("block",head_block_num()) );
|
||||
}
|
||||
|
||||
collateral_gathered += pays;
|
||||
const auto& order = *call_itr;
|
||||
++call_itr;
|
||||
FC_ASSERT( fill_order( order, pays, order.get_debt() ) );
|
||||
FC_ASSERT( fill_call_order( order, pays, order.get_debt(), settlement_price, true ) ); // call order is maker
|
||||
}
|
||||
|
||||
modify( bitasset, [&]( asset_bitasset_data_object& obj ){
|
||||
|
|
@ -93,7 +102,7 @@ void database::globally_settle_asset( const asset_object& mia, const price& sett
|
|||
|
||||
} FC_CAPTURE_AND_RETHROW( (mia)(settlement_price) ) }
|
||||
|
||||
void database::cancel_order(const force_settlement_object& order, bool create_virtual_op)
|
||||
void database::cancel_settle_order(const force_settlement_object& order, bool create_virtual_op)
|
||||
{
|
||||
adjust_balance(order.owner, order.balance);
|
||||
|
||||
|
|
@ -108,27 +117,98 @@ void database::cancel_order(const force_settlement_object& order, bool create_vi
|
|||
remove(order);
|
||||
}
|
||||
|
||||
void database::cancel_order( const limit_order_object& order, bool create_virtual_op )
|
||||
void database::cancel_settle_order( const limit_order_object& order, bool create_virtual_op, bool skip_cancel_fee )
|
||||
{
|
||||
auto refunded = order.amount_for_sale();
|
||||
|
||||
modify( order.seller(*this).statistics(*this),[&]( account_statistics_object& obj ){
|
||||
if( refunded.asset_id == asset_id_type() )
|
||||
{
|
||||
obj.total_core_in_orders -= refunded.amount;
|
||||
}
|
||||
});
|
||||
adjust_balance(order.seller, refunded);
|
||||
adjust_balance(order.seller, order.deferred_fee);
|
||||
|
||||
// if need to create a virtual op, try deduct a cancellation fee here.
|
||||
// there are two scenarios when order is cancelled and need to create a virtual op:
|
||||
// 1. due to expiration: always deduct a fee if there is any fee deferred
|
||||
// 2. due to cull_small: deduct a fee after hard fork 604, but not before (will set skip_cancel_fee)
|
||||
const account_statistics_object* seller_acc_stats = nullptr;
|
||||
const asset_dynamic_data_object* fee_asset_dyn_data = nullptr;
|
||||
limit_order_cancel_operation vop;
|
||||
share_type deferred_fee = order.deferred_fee;
|
||||
asset deferred_paid_fee = order.deferred_paid_fee;
|
||||
if( create_virtual_op )
|
||||
{
|
||||
limit_order_cancel_operation vop;
|
||||
vop.order = order.id;
|
||||
vop.fee_paying_account = order.seller;
|
||||
push_applied_operation( vop );
|
||||
// only deduct fee if not skipping fee, and there is any fee deferred
|
||||
if( !skip_cancel_fee && deferred_fee > 0 )
|
||||
{
|
||||
asset core_cancel_fee = current_fee_schedule().calculate_fee( vop );
|
||||
// cap the fee
|
||||
if( core_cancel_fee.amount > deferred_fee )
|
||||
core_cancel_fee.amount = deferred_fee;
|
||||
// if there is any CORE fee to deduct, redirect it to referral program
|
||||
if( core_cancel_fee.amount > 0 )
|
||||
{
|
||||
seller_acc_stats = &order.seller( *this ).statistics( *this );
|
||||
modify( *seller_acc_stats, [&]( account_statistics_object& obj ) {
|
||||
obj.pay_fee( core_cancel_fee.amount, get_global_properties().parameters.cashback_vesting_threshold );
|
||||
} );
|
||||
deferred_fee -= core_cancel_fee.amount;
|
||||
// handle originally paid fee if any:
|
||||
// to_deduct = round_up( paid_fee * core_cancel_fee / deferred_core_fee_before_deduct )
|
||||
if( deferred_paid_fee.amount == 0 )
|
||||
{
|
||||
vop.fee = core_cancel_fee;
|
||||
}
|
||||
else
|
||||
{
|
||||
fc::uint128 fee128( deferred_paid_fee.amount.value );
|
||||
fee128 *= core_cancel_fee.amount.value;
|
||||
// to round up
|
||||
fee128 += order.deferred_fee.value;
|
||||
fee128 -= 1;
|
||||
fee128 /= order.deferred_fee.value;
|
||||
share_type cancel_fee_amount = fee128.to_uint64();
|
||||
// cancel_fee should be positive, pay it to asset's accumulated_fees
|
||||
fee_asset_dyn_data = &deferred_paid_fee.asset_id(*this).dynamic_asset_data_id(*this);
|
||||
modify( *fee_asset_dyn_data, [&](asset_dynamic_data_object& addo) {
|
||||
addo.accumulated_fees += cancel_fee_amount;
|
||||
});
|
||||
// cancel_fee should be no more than deferred_paid_fee
|
||||
deferred_paid_fee.amount -= cancel_fee_amount;
|
||||
vop.fee = asset( cancel_fee_amount, deferred_paid_fee.asset_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// refund funds in order
|
||||
auto refunded = order.amount_for_sale();
|
||||
if( refunded.asset_id == asset_id_type() )
|
||||
{
|
||||
if( seller_acc_stats == nullptr )
|
||||
seller_acc_stats = &order.seller( *this ).statistics( *this );
|
||||
modify( *seller_acc_stats, [&]( account_statistics_object& obj ) {
|
||||
obj.total_core_in_orders -= refunded.amount;
|
||||
});
|
||||
}
|
||||
adjust_balance(order.seller, refunded);
|
||||
// refund fee
|
||||
// could be virtual op or real op here
|
||||
if( order.deferred_paid_fee.amount == 0 )
|
||||
{
|
||||
// be here, order.create_time <= HARDFORK_CORE_604_TIME, or fee paid in CORE, or no fee to refund.
|
||||
// if order was created before hard fork 604 then cancelled no matter before or after hard fork 604,
|
||||
// see it as fee paid in CORE, deferred_fee should be refunded to order owner but not fee pool
|
||||
adjust_balance( order.seller, deferred_fee );
|
||||
}
|
||||
else // need to refund fee in originally paid asset
|
||||
{
|
||||
adjust_balance(order.seller, deferred_paid_fee);
|
||||
// be here, must have: fee_asset != CORE
|
||||
if( fee_asset_dyn_data == nullptr )
|
||||
fee_asset_dyn_data = &deferred_paid_fee.asset_id(*this).dynamic_asset_data_id(*this);
|
||||
modify( *fee_asset_dyn_data, [&](asset_dynamic_data_object& addo) {
|
||||
addo.fee_pool += deferred_fee;
|
||||
});
|
||||
}
|
||||
|
||||
if( create_virtual_op )
|
||||
push_applied_operation( vop );
|
||||
|
||||
remove(order);
|
||||
}
|
||||
|
||||
|
|
@ -146,14 +226,19 @@ bool maybe_cull_small_order( database& db, const limit_order_object& order )
|
|||
*/
|
||||
if( order.amount_to_receive().amount == 0 )
|
||||
{
|
||||
//ilog( "applied epsilon logic" );
|
||||
db.cancel_order(order);
|
||||
if( order.deferred_fee > 0 && db.head_block_time() <= HARDFORK_CORE_604_TIME )
|
||||
{ // TODO remove this warning after hard fork core-604
|
||||
wlog( "At block ${n}, cancelling order without charging a fee: ${o}", ("n",db.head_block_num())("o",order) );
|
||||
db.cancel_limit_order( order, true, true );
|
||||
}
|
||||
else
|
||||
db.cancel_limit_order( order );
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool database::apply_order(const limit_order_object& new_order_object, bool allow_black_swan)
|
||||
bool database::apply_order_before_hardfork_625(const limit_order_object& new_order_object, bool allow_black_swan)
|
||||
{
|
||||
auto order_id = new_order_object.id;
|
||||
const asset_object& sell_asset = get(new_order_object.amount_for_sale().asset_id);
|
||||
|
|
@ -162,8 +247,8 @@ bool database::apply_order(const limit_order_object& new_order_object, bool allo
|
|||
// Possible optimization: We only need to check calls if both are true:
|
||||
// - The new order is at the front of the book
|
||||
// - The new order is below the call limit price
|
||||
bool called_some = check_call_orders(sell_asset, allow_black_swan);
|
||||
called_some |= check_call_orders(receive_asset, allow_black_swan);
|
||||
bool called_some = check_call_orders(sell_asset, allow_black_swan, true); // the first time when checking, call order is maker
|
||||
called_some |= check_call_orders(receive_asset, allow_black_swan, true); // the other side, same as above
|
||||
if( called_some && !find_object(order_id) ) // then we were filled by call order
|
||||
return true;
|
||||
|
||||
|
|
@ -189,8 +274,9 @@ bool database::apply_order(const limit_order_object& new_order_object, bool allo
|
|||
|
||||
//Possible optimization: only check calls if the new order completely filled some old order
|
||||
//Do I need to check both assets?
|
||||
check_call_orders(sell_asset, allow_black_swan);
|
||||
check_call_orders(receive_asset, allow_black_swan);
|
||||
check_call_orders(sell_asset, allow_black_swan); // after the new limit order filled some orders on the book,
|
||||
// if a call order matches another order, the call order is taker
|
||||
check_call_orders(receive_asset, allow_black_swan); // the other side, same as above
|
||||
|
||||
const limit_order_object* updated_order_object = find< limit_order_object >( order_id );
|
||||
if( updated_order_object == nullptr )
|
||||
|
|
@ -203,18 +289,144 @@ bool database::apply_order(const limit_order_object& new_order_object, bool allo
|
|||
return maybe_cull_small_order( *this, *updated_order_object );
|
||||
}
|
||||
|
||||
bool database::apply_order(const limit_order_object& new_order_object, bool allow_black_swan)
|
||||
{
|
||||
auto order_id = new_order_object.id;
|
||||
asset_id_type sell_asset_id = new_order_object.sell_asset_id();
|
||||
asset_id_type recv_asset_id = new_order_object.receive_asset_id();
|
||||
|
||||
// We only need to check if the new order will match with others if it is at the front of the book
|
||||
const auto& limit_price_idx = get_index_type<limit_order_index>().indices().get<by_price>();
|
||||
auto limit_itr = limit_price_idx.lower_bound( boost::make_tuple( new_order_object.sell_price, order_id ) );
|
||||
if( limit_itr != limit_price_idx.begin() )
|
||||
{
|
||||
--limit_itr;
|
||||
if( limit_itr->sell_asset_id() == sell_asset_id && limit_itr->receive_asset_id() == recv_asset_id )
|
||||
return false;
|
||||
}
|
||||
|
||||
// this is the opposite side (on the book)
|
||||
auto max_price = ~new_order_object.sell_price;
|
||||
limit_itr = limit_price_idx.lower_bound( max_price.max() );
|
||||
auto limit_end = limit_price_idx.upper_bound( max_price );
|
||||
|
||||
// Order matching should be in favor of the taker.
|
||||
// When a new limit order is created, e.g. an ask, need to check if it will match the highest bid.
|
||||
// We were checking call orders first. However, due to MSSR (maximum_short_squeeze_ratio),
|
||||
// effective price of call orders may be worse than limit orders, so we should also check limit orders here.
|
||||
|
||||
// Question: will a new limit order trigger a black swan event?
|
||||
//
|
||||
// 1. as of writing, it's possible due to the call-order-and-limit-order overlapping issue:
|
||||
// https://github.com/bitshares/bitshares-core/issues/606 .
|
||||
// when it happens, a call order can be very big but don't match with the opposite,
|
||||
// even when price feed is too far away, further than swan price,
|
||||
// if the new limit order is in the same direction with the call orders, it can eat up all the opposite,
|
||||
// then the call order will lose support and trigger a black swan event.
|
||||
// 2. after issue 606 is fixed, there will be no limit order on the opposite side "supporting" the call order,
|
||||
// so a new order in the same direction with the call order won't trigger a black swan event.
|
||||
// 3. calling is one direction. if the new limit order is on the opposite direction,
|
||||
// no matter if matches with the call, it won't trigger a black swan event.
|
||||
// (if a match at MSSP caused a black swan event, it means the call order is already undercollateralized,
|
||||
// which should trigger a black swan event earlier.)
|
||||
//
|
||||
// Since it won't trigger a black swan, no need to check here.
|
||||
|
||||
// currently we don't do cross-market (triangle) matching.
|
||||
// the limit order will only match with a call order if meet all of these:
|
||||
// 1. it's buying collateral, which means sell_asset is the MIA, receive_asset is the backing asset.
|
||||
// 2. sell_asset is not a prediction market
|
||||
// 3. sell_asset is not globally settled
|
||||
// 4. sell_asset has a valid price feed
|
||||
// 5. the call order's collateral ratio is below or equals to MCR
|
||||
// 6. the limit order provided a good price
|
||||
bool to_check_call_orders = false;
|
||||
const asset_object& sell_asset = sell_asset_id( *this );
|
||||
//const asset_object& recv_asset = recv_asset_id( *this );
|
||||
const asset_bitasset_data_object* sell_abd = nullptr;
|
||||
price call_match_price;
|
||||
if( sell_asset.is_market_issued() )
|
||||
{
|
||||
sell_abd = &sell_asset.bitasset_data( *this );
|
||||
if( sell_abd->options.short_backing_asset == recv_asset_id
|
||||
&& !sell_abd->is_prediction_market
|
||||
&& !sell_abd->has_settlement()
|
||||
&& !sell_abd->current_feed.settlement_price.is_null() )
|
||||
{
|
||||
call_match_price = ~sell_abd->current_feed.max_short_squeeze_price();
|
||||
if( ~new_order_object.sell_price <= call_match_price ) // new limit order price is good enough to match a call
|
||||
to_check_call_orders = true;
|
||||
}
|
||||
}
|
||||
|
||||
// this is the opposite side
|
||||
auto max_price = ~new_order_object.sell_price;
|
||||
limit_itr = limit_price_idx.lower_bound( max_price.max() );
|
||||
auto limit_end = limit_price_idx.upper_bound( max_price );
|
||||
bool to_check_limit_orders = (limit_itr != limit_end);
|
||||
|
||||
bool finished = false; // whether the new order is gone
|
||||
if( to_check_call_orders )
|
||||
{
|
||||
// check limit orders first, match the ones with better price in comparison to call orders
|
||||
while( !finished && limit_itr != limit_end && limit_itr->sell_price > call_match_price )
|
||||
{
|
||||
auto old_limit_itr = limit_itr;
|
||||
++limit_itr;
|
||||
// match returns 2 when only the old order was fully filled. In this case, we keep matching; otherwise, we stop.
|
||||
finished = ( match( new_order_object, *old_limit_itr, old_limit_itr->sell_price ) != 2 );
|
||||
}
|
||||
|
||||
if( !finished )
|
||||
{
|
||||
// check if there are margin calls
|
||||
const auto& call_price_idx = get_index_type<call_order_index>().indices().get<by_price>();
|
||||
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 )
|
||||
{
|
||||
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-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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// still need to check limit orders
|
||||
while( !finished && limit_itr != limit_end )
|
||||
{
|
||||
auto old_limit_itr = limit_itr;
|
||||
++limit_itr;
|
||||
// match returns 2 when only the old order was fully filled. In this case, we keep matching; otherwise, we stop.
|
||||
finished = ( match( new_order_object, *old_limit_itr, old_limit_itr->sell_price ) != 2 );
|
||||
|
||||
}
|
||||
|
||||
const limit_order_object* updated_order_object = find< limit_order_object >( order_id );
|
||||
if( updated_order_object == nullptr )
|
||||
return true;
|
||||
|
||||
// before #555 we would have done maybe_cull_small_order() logic as a result of fill_order() being called by match() above
|
||||
// however after #555 we need to get rid of small orders -- #555 hardfork defers logic that was done too eagerly before, and
|
||||
// this is the point it's deferred to.
|
||||
return maybe_cull_small_order( *this, *updated_order_object );
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the two orders,
|
||||
* Matches the two orders, the first parameter is taker, the second is maker.
|
||||
*
|
||||
* @return a bit field indicating which orders were filled (and thus removed)
|
||||
*
|
||||
* 0 - no orders were matched
|
||||
* 1 - bid was filled
|
||||
* 2 - ask was filled
|
||||
* 1 - taker was filled
|
||||
* 2 - maker was filled
|
||||
* 3 - both were filled
|
||||
*/
|
||||
template<typename OrderType>
|
||||
int database::match( const limit_order_object& usd, const OrderType& core, const price& match_price )
|
||||
int database::match( const limit_order_object& usd, const limit_order_object& core, const price& match_price )
|
||||
{
|
||||
assert( usd.sell_price.quote.asset_id == core.sell_price.base.asset_id );
|
||||
assert( usd.sell_price.base.asset_id == core.sell_price.quote.asset_id );
|
||||
|
|
@ -228,7 +440,7 @@ int database::match( const limit_order_object& usd, const OrderType& core, const
|
|||
if( usd_for_sale <= core_for_sale * match_price )
|
||||
{
|
||||
core_receives = usd_for_sale;
|
||||
usd_receives = usd_for_sale * match_price;
|
||||
usd_receives = usd_for_sale * match_price; // round down, in favor of bigger order
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -237,7 +449,7 @@ int database::match( const limit_order_object& usd, const OrderType& core, const
|
|||
//Although usd_for_sale is greater than core_for_sale * match_price, core_for_sale == usd_for_sale * match_price
|
||||
//Removing the assert seems to be safe -- apparently no asset is created or destroyed.
|
||||
usd_receives = core_for_sale;
|
||||
core_receives = core_for_sale * match_price;
|
||||
core_receives = core_for_sale * match_price; // round down, in favor of bigger order
|
||||
}
|
||||
|
||||
core_pays = usd_receives;
|
||||
|
|
@ -246,23 +458,84 @@ int database::match( const limit_order_object& usd, const OrderType& core, const
|
|||
assert( usd_pays == usd.amount_for_sale() ||
|
||||
core_pays == core.amount_for_sale() );
|
||||
|
||||
// Be here, it's possible that taker is paying something for nothing due to partially filled in last loop.
|
||||
// In this case, we see it as filled and cancel it later
|
||||
// The maker won't be paying something for nothing, since if it would, it would have been cancelled already.
|
||||
if( usd_receives.amount == 0 && get_dynamic_global_properties().next_maintenance_time > HARDFORK_CORE_184_TIME )
|
||||
return 1;
|
||||
|
||||
int result = 0;
|
||||
result |= fill_order( usd, usd_pays, usd_receives, false );
|
||||
result |= fill_order( core, core_pays, core_receives, true ) << 1;
|
||||
result |= fill_limit_order( usd, usd_pays, usd_receives, false, match_price, false ); // the first param is taker
|
||||
result |= fill_limit_order( core, core_pays, core_receives, true, match_price, true ) << 1; // the second param is maker
|
||||
assert( result != 0 );
|
||||
return result;
|
||||
}
|
||||
|
||||
int database::match( const limit_order_object& bid, const limit_order_object& ask, const price& match_price )
|
||||
int database::match( const limit_order_object& bid, const call_order_object& ask, const price& match_price )
|
||||
{
|
||||
return match<limit_order_object>( bid, ask, match_price );
|
||||
FC_ASSERT( bid.sell_asset_id() == ask.debt_type() );
|
||||
FC_ASSERT( bid.receive_asset_id() == ask.collateral_type() );
|
||||
FC_ASSERT( bid.for_sale > 0 && ask.debt > 0 && ask.collateral > 0 );
|
||||
|
||||
bool filled_limit = false;
|
||||
bool filled_call = false;
|
||||
|
||||
asset usd_for_sale = bid.amount_for_sale();
|
||||
asset usd_to_buy = ask.get_debt();
|
||||
|
||||
asset call_pays, call_receives, order_pays, order_receives;
|
||||
if( usd_to_buy >= usd_for_sale )
|
||||
{ // fill limit order
|
||||
call_receives = usd_for_sale;
|
||||
order_receives = usd_for_sale * match_price; // round down here, in favor of call order
|
||||
call_pays = order_receives;
|
||||
order_pays = usd_for_sale;
|
||||
|
||||
filled_limit = true;
|
||||
filled_call = ( usd_to_buy == usd_for_sale );
|
||||
}
|
||||
else
|
||||
{ // fill call order
|
||||
call_receives = usd_to_buy;
|
||||
order_receives = usd_to_buy * match_price; // round down here, in favor of call order
|
||||
call_pays = order_receives;
|
||||
order_pays = usd_to_buy;
|
||||
|
||||
filled_call = true;
|
||||
}
|
||||
|
||||
FC_ASSERT( filled_call || filled_limit );
|
||||
|
||||
// Be here, it's possible that taker is paying something for nothing.
|
||||
// The maker won't be paying something for nothing according to code above
|
||||
if( order_receives.amount == 0 && get_dynamic_global_properties().next_maintenance_time > HARDFORK_CORE_184_TIME )
|
||||
{
|
||||
// It's possible that taker is paying something for nothing due to call order too small.
|
||||
// In this case, let the call pay 1 Satoshi
|
||||
if( filled_call )
|
||||
{
|
||||
order_receives.amount = 1;
|
||||
call_pays = order_receives;
|
||||
}
|
||||
else
|
||||
// It's possible that taker is paying something for nothing due to partially filled in last loop.
|
||||
// In this case, we see it as filled and cancel it later
|
||||
return 1;
|
||||
}
|
||||
|
||||
int result = 0;
|
||||
result |= fill_limit_order( bid, order_pays, order_receives, false, 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 );
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
asset database::match( const call_order_object& call,
|
||||
const force_settlement_object& settle,
|
||||
const price& match_price,
|
||||
asset max_settlement )
|
||||
asset max_settlement,
|
||||
const price& fill_price )
|
||||
{ try {
|
||||
FC_ASSERT(call.get_debt().asset_id == settle.balance.asset_id );
|
||||
FC_ASSERT(call.debt > 0 && call.collateral > 0 && settle.balance.amount > 0);
|
||||
|
|
@ -271,7 +544,34 @@ asset database::match( const call_order_object& call,
|
|||
auto call_debt = call.get_debt();
|
||||
|
||||
asset call_receives = std::min(settle_for_sale, call_debt);
|
||||
asset call_pays = call_receives * match_price;
|
||||
asset call_pays = call_receives * match_price; // round down here, in favor of call order
|
||||
|
||||
// Be here, the call order may be paying nothing.
|
||||
if( call_pays.amount == 0 )
|
||||
{
|
||||
if( get_dynamic_global_properties().next_maintenance_time > HARDFORK_CORE_184_TIME )
|
||||
{
|
||||
if( call_receives == call_debt ) // the call order is smaller than or equal to the settle order
|
||||
{
|
||||
wlog( "Something for nothing issue (#184, variant C-1) handled at block #${block}", ("block",head_block_num()) );
|
||||
call_pays.amount = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if( call_receives == settle.balance ) // the settle order is smaller
|
||||
{
|
||||
wlog( "Something for nothing issue (#184, variant C-2) handled at block #${block}", ("block",head_block_num()) );
|
||||
cancel_settle_order( settle );
|
||||
}
|
||||
else // neither order will be completely filled, perhaps due to max_settlement too small
|
||||
wlog( "Something for nothing issue (#184, variant C-3) handled at block #${block}", ("block",head_block_num()) );
|
||||
return asset( 0, settle.balance.asset_id );
|
||||
}
|
||||
}
|
||||
else
|
||||
wlog( "Something for nothing issue (#184, variant C) occurred at block #${block}", ("block",head_block_num()) );
|
||||
}
|
||||
|
||||
asset settle_pays = call_receives;
|
||||
asset settle_receives = call_pays;
|
||||
|
||||
|
|
@ -286,13 +586,14 @@ asset database::match( const call_order_object& call,
|
|||
|
||||
assert( settle_pays == settle_for_sale || call_receives == call.get_debt() );
|
||||
|
||||
fill_order(call, call_pays, call_receives);
|
||||
fill_order(settle, settle_pays, settle_receives);
|
||||
fill_call_order( call, call_pays, call_receives, fill_price, true ); // call order is maker
|
||||
fill_settle_order( settle, settle_pays, settle_receives, fill_price, false ); // force settlement order is taker
|
||||
|
||||
return call_receives;
|
||||
} FC_CAPTURE_AND_RETHROW( (call)(settle)(match_price)(max_settlement) ) }
|
||||
|
||||
bool database::fill_order( const limit_order_object& order, const asset& pays, const asset& receives, bool cull_if_small )
|
||||
bool database::fill_limit_order( const limit_order_object& order, const asset& pays, const asset& receives, bool cull_if_small,
|
||||
const price& fill_price, const bool is_maker )
|
||||
{ try {
|
||||
cull_if_small |= (head_block_time() < HARDFORK_555_TIME);
|
||||
|
||||
|
|
@ -306,7 +607,7 @@ bool database::fill_order( const limit_order_object& order, const asset& pays, c
|
|||
pay_order( seller, receives - issuer_fees, pays );
|
||||
|
||||
assert( pays.asset_id != receives.asset_id );
|
||||
push_applied_operation( fill_order_operation( order.id, order.seller, pays, receives, issuer_fees ) );
|
||||
push_applied_operation( fill_order_operation( order.id, order.seller, pays, receives, issuer_fees, fill_price, is_maker ) );
|
||||
|
||||
// conditional because cheap integer comparison may allow us to avoid two expensive modify() and object lookups
|
||||
if( order.deferred_fee > 0 )
|
||||
|
|
@ -317,6 +618,14 @@ bool database::fill_order( const limit_order_object& order, const asset& pays, c
|
|||
} );
|
||||
}
|
||||
|
||||
if( order.deferred_paid_fee.amount > 0 ) // implies head_block_time() > HARDFORK_CORE_604_TIME
|
||||
{
|
||||
const auto& fee_asset_dyn_data = order.deferred_paid_fee.asset_id(*this).dynamic_asset_data_id(*this);
|
||||
modify( fee_asset_dyn_data, [&](asset_dynamic_data_object& addo) {
|
||||
addo.accumulated_fees += order.deferred_paid_fee.amount;
|
||||
});
|
||||
}
|
||||
|
||||
if( pays == order.amount_for_sale() )
|
||||
{
|
||||
remove( order );
|
||||
|
|
@ -327,6 +636,7 @@ bool database::fill_order( const limit_order_object& order, const asset& pays, c
|
|||
modify( order, [&]( limit_order_object& b ) {
|
||||
b.for_sale -= pays.amount;
|
||||
b.deferred_fee = 0;
|
||||
b.deferred_paid_fee.amount = 0;
|
||||
});
|
||||
if( cull_if_small )
|
||||
return maybe_cull_small_order( *this, order );
|
||||
|
|
@ -335,13 +645,17 @@ bool database::fill_order( const limit_order_object& order, const asset& pays, c
|
|||
} FC_CAPTURE_AND_RETHROW( (order)(pays)(receives) ) }
|
||||
|
||||
|
||||
bool database::fill_order( const call_order_object& order, const asset& pays, const asset& receives )
|
||||
bool database::fill_call_order( const call_order_object& order, const asset& pays, const asset& receives,
|
||||
const price& fill_price, const bool is_maker )
|
||||
{ try {
|
||||
//idump((pays)(receives)(order));
|
||||
FC_ASSERT( order.get_debt().asset_id == receives.asset_id );
|
||||
FC_ASSERT( order.get_collateral().asset_id == pays.asset_id );
|
||||
FC_ASSERT( order.get_collateral() >= pays );
|
||||
|
||||
const asset_object& mia = receives.asset_id(*this);
|
||||
FC_ASSERT( mia.is_market_issued() );
|
||||
|
||||
optional<asset> collateral_freed;
|
||||
modify( order, [&]( call_order_object& o ){
|
||||
o.debt -= receives.amount;
|
||||
|
|
@ -351,9 +665,10 @@ bool database::fill_order( const call_order_object& order, const asset& pays, co
|
|||
collateral_freed = o.get_collateral();
|
||||
o.collateral = 0;
|
||||
}
|
||||
else if( get_dynamic_global_properties().next_maintenance_time > HARDFORK_CORE_343_TIME )
|
||||
o.call_price = price::call_price( o.get_debt(), o.get_collateral(),
|
||||
mia.bitasset_data(*this).current_feed.maintenance_collateral_ratio );
|
||||
});
|
||||
const asset_object& mia = receives.asset_id(*this);
|
||||
assert( mia.is_market_issued() );
|
||||
|
||||
const asset_dynamic_data_object& mia_ddo = mia.dynamic_asset_data_id(*this);
|
||||
|
||||
|
|
@ -370,7 +685,7 @@ bool database::fill_order( const call_order_object& order, const asset& pays, co
|
|||
adjust_balance(borrower.get_id(), *collateral_freed);
|
||||
|
||||
modify( borrower_statistics, [&]( account_statistics_object& b ){
|
||||
if( collateral_freed && collateral_freed->amount > 0 )
|
||||
if( collateral_freed && 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;
|
||||
|
|
@ -380,7 +695,8 @@ bool database::fill_order( const call_order_object& order, const asset& pays, co
|
|||
}
|
||||
|
||||
assert( pays.asset_id != receives.asset_id );
|
||||
push_applied_operation( fill_order_operation{ order.id, order.borrower, pays, receives, asset(0, pays.asset_id) } );
|
||||
push_applied_operation( fill_order_operation( order.id, order.borrower, pays, receives,
|
||||
asset(0, pays.asset_id), fill_price, is_maker ) );
|
||||
|
||||
if( collateral_freed )
|
||||
remove( order );
|
||||
|
|
@ -388,7 +704,8 @@ bool database::fill_order( const call_order_object& order, const asset& pays, co
|
|||
return collateral_freed.valid();
|
||||
} FC_CAPTURE_AND_RETHROW( (order)(pays)(receives) ) }
|
||||
|
||||
bool database::fill_order(const force_settlement_object& settle, const asset& pays, const asset& receives)
|
||||
bool database::fill_settle_order( const force_settlement_object& settle, const asset& pays, const asset& receives,
|
||||
const price& fill_price, const bool is_maker )
|
||||
{ try {
|
||||
bool filled = false;
|
||||
|
||||
|
|
@ -406,7 +723,7 @@ bool database::fill_order(const force_settlement_object& settle, const asset& pa
|
|||
adjust_balance(settle.owner, receives - issuer_fees);
|
||||
|
||||
assert( pays.asset_id != receives.asset_id );
|
||||
push_applied_operation( fill_order_operation{ settle.id, settle.owner, pays, receives, issuer_fees } );
|
||||
push_applied_operation( fill_order_operation( settle.id, settle.owner, pays, receives, issuer_fees, fill_price, is_maker ) );
|
||||
|
||||
if (filled)
|
||||
remove(settle);
|
||||
|
|
@ -471,8 +788,11 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa
|
|||
|
||||
bool after_hardfork_436 = ( head_time > HARDFORK_436_TIME );
|
||||
|
||||
auto head_time = head_block_time();
|
||||
auto maint_time = get_dynamic_global_properties().next_maintenance_time;
|
||||
while( !check_for_blackswan( mia, enable_black_swan, &bitasset ) && call_itr != call_end )
|
||||
{
|
||||
bool filled_limit_in_loop = false;
|
||||
bool filled_call = false;
|
||||
price match_price;
|
||||
asset usd_for_sale;
|
||||
|
|
@ -486,27 +806,15 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa
|
|||
|
||||
match_price.validate();
|
||||
|
||||
// would be margin called, but there is no matching order #436
|
||||
bool feed_protected = ( bitasset.current_feed.settlement_price > ~call_itr->call_price );
|
||||
if( feed_protected && after_hardfork_436 )
|
||||
// Feed protected (don't call if CR>MCR) https://github.com/cryptonomex/graphene/issues/436
|
||||
if( ( head_time > HARDFORK_436_TIME )
|
||||
&& ( bitasset.current_feed.settlement_price > ~call_itr->call_price ) )
|
||||
return margin_called;
|
||||
|
||||
// would be margin called, but there is no matching order
|
||||
if( match_price > ~call_itr->call_price )
|
||||
// Old rule: margin calls can only buy high https://github.com/bitshares/bitshares-core/issues/606
|
||||
if( maint_time <= HARDFORK_CORE_606_TIME && match_price > ~call_itr->call_price )
|
||||
return margin_called;
|
||||
|
||||
if( feed_protected )
|
||||
{
|
||||
ilog( "Feed protected margin call executing (HARDFORK_436_TIME not here yet)" );
|
||||
idump( (*call_itr) );
|
||||
idump( (*limit_itr) );
|
||||
}
|
||||
|
||||
// idump((*call_itr));
|
||||
// idump((*limit_itr));
|
||||
|
||||
// ilog( "match_price <= ~call_itr->call_price performing a margin call" );
|
||||
|
||||
margin_called = true;
|
||||
|
||||
auto usd_to_buy = call_itr->get_debt();
|
||||
|
|
@ -525,31 +833,67 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa
|
|||
if( usd_to_buy >= usd_for_sale )
|
||||
{ // fill order
|
||||
call_receives = usd_for_sale;
|
||||
order_receives = usd_for_sale * match_price;
|
||||
order_receives = usd_for_sale * match_price; // round down, in favor of call order
|
||||
call_pays = order_receives;
|
||||
order_pays = usd_for_sale;
|
||||
|
||||
filled_limit_in_loop = true;
|
||||
filled_limit = true;
|
||||
filled_call = (usd_to_buy == usd_for_sale);
|
||||
} else { // fill call
|
||||
call_receives = usd_to_buy;
|
||||
order_receives = usd_to_buy * match_price;
|
||||
order_receives = usd_to_buy * match_price; // round down, in favor of call order
|
||||
call_pays = order_receives;
|
||||
order_pays = usd_to_buy;
|
||||
|
||||
filled_call = true;
|
||||
if( filled_limit && maint_time <= HARDFORK_CORE_453_TIME )
|
||||
wlog( "Multiple limit match problem (issue 453) occurred at block #${block}", ("block",head_block_num()) );
|
||||
}
|
||||
|
||||
FC_ASSERT( filled_call || filled_limit );
|
||||
FC_ASSERT( filled_call || filled_limit_in_loop );
|
||||
|
||||
// Be here, the call order won't be paying something for nothing according to code above.
|
||||
// After hard fork core-625, the limit order will be always a maker if entered this function,
|
||||
// so it won't be paying something for nothing, since if it would, it would have been cancelled already.
|
||||
// However, we need to check if it's culled after partially filled.
|
||||
// Before hard fork core-625,
|
||||
// when the limit order is a taker, it could be paying something for nothing here;
|
||||
// when the limit order is a maker, it won't be paying something for nothing,
|
||||
// however, if it's culled after partially filled, `limit_itr` may be invalidated so should not be dereferenced
|
||||
if( order_receives.amount == 0 )
|
||||
{
|
||||
if( maint_time > HARDFORK_CORE_184_TIME )
|
||||
{
|
||||
if( filled_call ) // call would be completely filled // should always be true
|
||||
{
|
||||
order_receives.amount = 1; // round up to 1 Satoshi
|
||||
call_pays = order_receives;
|
||||
}
|
||||
// else do nothing, since the limit order should have already been cancelled elsewhere
|
||||
else // TODO remove warning after confirmed
|
||||
wlog( "Something for nothing issue (#184, variant D-1) occurred at block #${block}", ("block",head_block_num()) );
|
||||
}
|
||||
else // TODO remove warning after hard fork core-184
|
||||
wlog( "Something for nothing issue (#184, variant D) occurred at block #${block}", ("block",head_block_num()) );
|
||||
}
|
||||
|
||||
auto old_call_itr = call_itr;
|
||||
if( filled_call ) ++call_itr;
|
||||
fill_order(*old_call_itr, call_pays, call_receives);
|
||||
if( filled_call && maint_time <= HARDFORK_CORE_343_TIME )
|
||||
++call_itr;
|
||||
// when for_new_limit_order is true, the call order is maker, otherwise the call order is taker
|
||||
fill_call_order(*old_call_itr, call_pays, call_receives, match_price, for_new_limit_order );
|
||||
if( maint_time > HARDFORK_CORE_343_TIME )
|
||||
call_itr = call_price_index.lower_bound( call_min );
|
||||
|
||||
auto old_limit_itr = filled_limit ? limit_itr++ : limit_itr;
|
||||
fill_order(*old_limit_itr, order_pays, order_receives, true);
|
||||
auto next_limit_itr = std::next( limit_itr );
|
||||
// when for_new_limit_order is true, the limit order is taker, otherwise the limit order is maker
|
||||
bool really_filled = fill_limit_order( *limit_itr, order_pays, order_receives, true, match_price, !for_new_limit_order );
|
||||
if( really_filled || ( filled_limit && maint_time <= HARDFORK_CORE_453_TIME ) )
|
||||
limit_itr = next_limit_itr;
|
||||
|
||||
} // whlie call_itr != call_end
|
||||
} // while call_itr != call_end
|
||||
|
||||
return margin_called;
|
||||
} FC_CAPTURE_AND_RETHROW() }
|
||||
|
|
|
|||
|
|
@ -253,19 +253,6 @@ bool database::check_for_blackswan( const asset_object& mia, bool enable_black_s
|
|||
const call_order_index& call_index = get_index_type<call_order_index>();
|
||||
const auto& call_price_index = call_index.indices().get<by_price>();
|
||||
|
||||
const limit_order_index& limit_index = get_index_type<limit_order_index>();
|
||||
const auto& limit_price_index = limit_index.indices().get<by_price>();
|
||||
|
||||
// looking for limit orders selling the most USD for the least CORE
|
||||
auto highest_possible_bid = price::max( mia.id, bitasset.options.short_backing_asset );
|
||||
// stop when limit orders are selling too little USD for too much CORE
|
||||
auto lowest_possible_bid = price::min( mia.id, bitasset.options.short_backing_asset );
|
||||
|
||||
assert( highest_possible_bid.base.asset_id == lowest_possible_bid.base.asset_id );
|
||||
// NOTE limit_price_index is sorted from greatest to least
|
||||
auto limit_itr = limit_price_index.lower_bound( highest_possible_bid );
|
||||
auto limit_end = limit_price_index.upper_bound( lowest_possible_bid );
|
||||
|
||||
auto call_min = price::min( bitasset.options.short_backing_asset, mia.id );
|
||||
auto call_max = price::max( bitasset.options.short_backing_asset, mia.id );
|
||||
auto call_itr = call_price_index.lower_bound( call_min );
|
||||
|
|
@ -274,25 +261,49 @@ bool database::check_for_blackswan( const asset_object& mia, bool enable_black_s
|
|||
if( call_itr == call_end ) return false; // no call orders
|
||||
|
||||
price highest = settle_price;
|
||||
|
||||
auto maint_time = get_dynamic_global_properties().next_maintenance_time;
|
||||
if( maint_time > HARDFORK_CORE_338_TIME )
|
||||
// due to #338, we won't check for black swan on incoming limit order, so need to check with MSSP here
|
||||
highest = bitasset.current_feed.max_short_squeeze_price();
|
||||
|
||||
const limit_order_index& limit_index = get_index_type<limit_order_index>();
|
||||
const auto& limit_price_index = limit_index.indices().get<by_price>();
|
||||
|
||||
// looking for limit orders selling the most USD for the least CORE
|
||||
auto highest_possible_bid = price::max( mia.id, bitasset.options.short_backing_asset );
|
||||
// stop when limit orders are selling too little USD for too much CORE
|
||||
auto lowest_possible_bid = price::min( mia.id, bitasset.options.short_backing_asset );
|
||||
|
||||
FC_ASSERT( highest_possible_bid.base.asset_id == lowest_possible_bid.base.asset_id );
|
||||
// NOTE limit_price_index is sorted from greatest to least
|
||||
auto limit_itr = limit_price_index.lower_bound( highest_possible_bid );
|
||||
auto limit_end = limit_price_index.upper_bound( lowest_possible_bid );
|
||||
|
||||
if( limit_itr != limit_end ) {
|
||||
assert( settle_price.base.asset_id == limit_itr->sell_price.base.asset_id );
|
||||
highest = std::max( limit_itr->sell_price, settle_price );
|
||||
FC_ASSERT( highest.base.asset_id == limit_itr->sell_price.base.asset_id );
|
||||
highest = std::max( limit_itr->sell_price, highest );
|
||||
}
|
||||
|
||||
auto least_collateral = call_itr->collateralization();
|
||||
if( ~least_collateral >= highest )
|
||||
{
|
||||
wdump( (*call_itr) );
|
||||
elog( "Black Swan detected: \n"
|
||||
" Least collateralized call: ${lc} ${~lc}\n"
|
||||
// " Highest Bid: ${hb} ${~hb}\n"
|
||||
" Settle Price: ${sp} ${~sp}\n"
|
||||
" Max: ${h} ${~h}\n",
|
||||
" Settle Price: ${~sp} ${sp}\n"
|
||||
" Max: ${~h} ${h}\n",
|
||||
("lc",least_collateral.to_real())("~lc",(~least_collateral).to_real())
|
||||
// ("hb",limit_itr->sell_price.to_real())("~hb",(~limit_itr->sell_price).to_real())
|
||||
("sp",settle_price.to_real())("~sp",(~settle_price).to_real())
|
||||
("h",highest.to_real())("~h",(~highest).to_real()) );
|
||||
FC_ASSERT( enable_black_swan, "Black swan was detected during a margin update which is not allowed to trigger a blackswan" );
|
||||
globally_settle_asset(mia, ~least_collateral );
|
||||
if( maint_time > HARDFORK_CORE_338_TIME && ~least_collateral <= settle_price )
|
||||
// globol settle at feed price if possible
|
||||
globally_settle_asset(mia, settle_price );
|
||||
else
|
||||
globally_settle_asset(mia, ~least_collateral );
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -300,33 +311,25 @@ bool database::check_for_blackswan( const asset_object& mia, bool enable_black_s
|
|||
|
||||
void database::clear_expired_orders()
|
||||
{ try {
|
||||
detail::with_skip_flags( *this,
|
||||
get_node_properties().skip_flags | skip_authority_check, [&](){
|
||||
transaction_evaluation_state cancel_context(this);
|
||||
|
||||
//Cancel expired limit orders
|
||||
auto& limit_index = get_index_type<limit_order_index>().indices().get<by_expiration>();
|
||||
while( !limit_index.empty() && limit_index.begin()->expiration <= head_block_time() )
|
||||
{
|
||||
limit_order_cancel_operation canceler;
|
||||
const limit_order_object& order = *limit_index.begin();
|
||||
canceler.fee_paying_account = order.seller;
|
||||
canceler.order = order.id;
|
||||
canceler.fee = current_fee_schedule().calculate_fee( canceler );
|
||||
if( canceler.fee.amount > order.deferred_fee )
|
||||
auto base_asset = order.sell_price.base.asset_id;
|
||||
auto quote_asset = order.sell_price.quote.asset_id;
|
||||
cancel_limit_order( order );
|
||||
// check call orders
|
||||
if( head_block_time() > HARDFORK_CORE_604_TIME )
|
||||
{
|
||||
// Cap auto-cancel fees at deferred_fee; see #549
|
||||
wlog( "At block ${b}, fee for clearing expired order ${oid} was capped at deferred_fee ${fee}", ("b", head_block_num())("oid", order.id)("fee", order.deferred_fee) );
|
||||
canceler.fee = asset( order.deferred_fee, asset_id_type() );
|
||||
// Comments below are copied from limit_order_cancel_evaluator::do_apply(...)
|
||||
// Possible optimization: order can be called by cancelling a limit order
|
||||
// if the canceled order was at the top of the book.
|
||||
// Do I need to check calls in both assets?
|
||||
check_call_orders( base_asset( *this ) );
|
||||
check_call_orders( quote_asset( *this ) );
|
||||
}
|
||||
// we know the fee for this op is set correctly since it is set by the chain.
|
||||
// this allows us to avoid a hung chain:
|
||||
// - if #549 case above triggers
|
||||
// - if the fee is incorrect, which may happen due to #435 (although since cancel is a fixed-fee op, it shouldn't)
|
||||
cancel_context.skip_fee_schedule_check = true;
|
||||
apply_operation(cancel_context, canceler);
|
||||
}
|
||||
});
|
||||
|
||||
//Process expired force settlement orders
|
||||
auto& settlement_index = get_index_type<force_settlement_index>().indices().get<by_expiration>();
|
||||
|
|
@ -334,9 +337,11 @@ void database::clear_expired_orders()
|
|||
{
|
||||
asset_id_type current_asset = settlement_index.begin()->settlement_asset_id();
|
||||
asset max_settlement_volume;
|
||||
price settlement_fill_price;
|
||||
bool current_asset_finished = false;
|
||||
bool extra_dump = false;
|
||||
|
||||
auto next_asset = [¤t_asset, &settlement_index, &extra_dump] {
|
||||
auto next_asset = [¤t_asset, ¤t_asset_finished, &settlement_index, &extra_dump] {
|
||||
auto bound = settlement_index.upper_bound(current_asset);
|
||||
if( bound == settlement_index.end() )
|
||||
{
|
||||
|
|
@ -351,6 +356,7 @@ void database::clear_expired_orders()
|
|||
ilog( "next_asset returning true, bound is ${b}", ("b", *bound) );
|
||||
}
|
||||
current_asset = bound->settlement_asset_id();
|
||||
current_asset_finished = false;
|
||||
return true;
|
||||
};
|
||||
|
||||
|
|
@ -379,7 +385,7 @@ void database::clear_expired_orders()
|
|||
if( mia.has_settlement() )
|
||||
{
|
||||
ilog( "Canceling a force settlement because of black swan" );
|
||||
cancel_order( order );
|
||||
cancel_settle_order( order );
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -401,12 +407,14 @@ void database::clear_expired_orders()
|
|||
{
|
||||
ilog("Canceling a force settlement in ${asset} because settlement price is null",
|
||||
("asset", mia_object.symbol));
|
||||
cancel_order(order);
|
||||
cancel_settle_order(order);
|
||||
continue;
|
||||
}
|
||||
if( max_settlement_volume.asset_id != current_asset )
|
||||
max_settlement_volume = mia_object.amount(mia.max_force_settlement_volume(mia_object.dynamic_data(*this).current_supply));
|
||||
if( mia.force_settled_volume >= max_settlement_volume.amount )
|
||||
// When current_asset_finished is true, this would be the 2nd time processing the same order.
|
||||
// In this case, we move to the next asset.
|
||||
if( mia.force_settled_volume >= max_settlement_volume.amount || current_asset_finished )
|
||||
{
|
||||
/*
|
||||
ilog("Skipping force settlement in ${asset}; settled ${settled_volume} / ${max_volume}",
|
||||
|
|
@ -432,6 +440,16 @@ void database::clear_expired_orders()
|
|||
|
||||
price settlement_price = pays / receives;
|
||||
|
||||
// Calculate fill_price with a bigger volume to reduce impacts of rounding
|
||||
if( settlement_fill_price.base.asset_id != pays.asset_id )
|
||||
{
|
||||
asset tmp_pays = max_settlement_volume;
|
||||
asset tmp_receives = tmp_pays * mia.current_feed.settlement_price;
|
||||
tmp_receives.amount = (fc::uint128_t(tmp_receives.amount.value) *
|
||||
(GRAPHENE_100_PERCENT - mia.options.force_settlement_offset_percent) / GRAPHENE_100_PERCENT).to_uint64();
|
||||
settlement_fill_price = tmp_pays / tmp_receives;
|
||||
}
|
||||
|
||||
auto& call_index = get_index_type<call_order_index>().indices().get<by_collateral>();
|
||||
asset settled = mia_object.amount(mia.force_settled_volume);
|
||||
// Match against the least collateralized short until the settlement is finished or we reach max settlements
|
||||
|
|
@ -446,15 +464,22 @@ void database::clear_expired_orders()
|
|||
if( order.balance.amount == 0 )
|
||||
{
|
||||
wlog( "0 settlement detected" );
|
||||
cancel_order( order );
|
||||
cancel_settle_order( order );
|
||||
break;
|
||||
}
|
||||
try {
|
||||
settled += match(*itr, order, settlement_price, max_settlement);
|
||||
asset new_settled = match(*itr, order, settlement_price, max_settlement, settlement_fill_price);
|
||||
if( maint_time > HARDFORK_CORE_184_TIME && new_settled.amount == 0 ) // unable to fill this settle order
|
||||
{
|
||||
if( find_object( order_id ) ) // the settle order hasn't been cancelled
|
||||
current_asset_finished = true;
|
||||
break;
|
||||
}
|
||||
settled += new_settled;
|
||||
}
|
||||
catch ( const black_swan_exception& e ) {
|
||||
wlog( "black swan detected: ${e}", ("e", e.to_detail_string() ) );
|
||||
cancel_order( order );
|
||||
cancel_settle_order( order );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
libraries/chain/hardfork.d/CORE_184.hf
Normal file
4
libraries/chain/hardfork.d/CORE_184.hf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// bitshares-core issue #184 Fix "Potential something-for-nothing fill bug"
|
||||
#ifndef HARDFORK_CORE_184_TIME
|
||||
#define HARDFORK_CORE_184_TIME (fc::time_point_sec( 1615334400 )) // Wednesday, 10 March 2021 00:00:00 UTC
|
||||
#endif
|
||||
4
libraries/chain/hardfork.d/CORE_338.hf
Normal file
4
libraries/chain/hardfork.d/CORE_338.hf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// bitshares-core issue #338 Fix "margin call order fills at price of matching limit_order"
|
||||
#ifndef HARDFORK_CORE_338_TIME
|
||||
#define HARDFORK_CORE_338_TIME (fc::time_point_sec( 1615334400 )) // Wednesday, 10 March 2021 00:00:00 UTC
|
||||
#endif
|
||||
5
libraries/chain/hardfork.d/CORE_342.hf
Normal file
5
libraries/chain/hardfork.d/CORE_342.hf
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// bitshares-core issue #342
|
||||
// Mitigate rounding issue when matching orders
|
||||
#ifndef HARDFORK_CORE_342_TIME
|
||||
#define HARDFORK_CORE_342_TIME (fc::time_point_sec( 1615334400 )) // Wednesday, 10 March 2021 00:00:00 UTC
|
||||
#endif
|
||||
5
libraries/chain/hardfork.d/CORE_343.hf
Normal file
5
libraries/chain/hardfork.d/CORE_343.hf
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// bitshares-core issue #343
|
||||
// Fix "Inconsistent sorting of call orders between matching against a limit order and a force settle order"
|
||||
#ifndef HARDFORK_CORE_343_TIME
|
||||
#define HARDFORK_CORE_343_TIME (fc::time_point_sec( 1615334400 )) // Wednesday, 10 March 2021 00:00:00 UTC
|
||||
#endif
|
||||
4
libraries/chain/hardfork.d/CORE_453.hf
Normal file
4
libraries/chain/hardfork.d/CORE_453.hf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// bitshares-core issue #453 Fix "Multiple limit order and call order matching issue"
|
||||
#ifndef HARDFORK_CORE_453_TIME
|
||||
#define HARDFORK_CORE_453_TIME (fc::time_point_sec( 1615334400 )) // Wednesday, 10 March 2021 00:00:00 UTC
|
||||
#endif
|
||||
5
libraries/chain/hardfork.d/CORE_604.hf
Normal file
5
libraries/chain/hardfork.d/CORE_604.hf
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// bitshares-core issue #604
|
||||
// Implement BSIP 26: refund order creation fee in original paid asset when order is cancelled
|
||||
#ifndef HARDFORK_CORE_604_TIME
|
||||
#define HARDFORK_CORE_604_TIME (fc::time_point_sec( 1615334400 )) // Wednesday, 10 March 2021 00:00:00 UTC
|
||||
#endif
|
||||
4
libraries/chain/hardfork.d/CORE_606.hf
Normal file
4
libraries/chain/hardfork.d/CORE_606.hf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// bitshares-core issue #606 Fix "Undercollateralized short positions should be called regardless of asks"
|
||||
#ifndef HARDFORK_CORE_606_TIME
|
||||
#define HARDFORK_CORE_606_TIME (fc::time_point_sec( 1615334400 )) // Wednesday, 10 March 2021 00:00:00 UTC
|
||||
#endif
|
||||
4
libraries/chain/hardfork.d/CORE_625.hf
Normal file
4
libraries/chain/hardfork.d/CORE_625.hf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// bitshares-core issue #625 Fix "Potential erratic order matching issue involving margin call orders"
|
||||
#ifndef HARDFORK_CORE_625_TIME
|
||||
#define HARDFORK_CORE_625_TIME (fc::time_point_sec( 1615334400 )) // Wednesday, 10 March 2021 00:00:00 UTC
|
||||
#endif
|
||||
|
|
@ -407,8 +407,8 @@ namespace graphene { namespace chain {
|
|||
|
||||
/// @{ @group Market Helpers
|
||||
void globally_settle_asset( const asset_object& bitasset, const price& settle_price );
|
||||
void cancel_order(const force_settlement_object& order, bool create_virtual_op = true);
|
||||
void cancel_order(const limit_order_object& order, bool create_virtual_op = true);
|
||||
void cancel_settle_order(const force_settlement_object& order, bool create_virtual_op = true);
|
||||
void cancel_limit_order(const limit_order_object& order, bool create_virtual_op = true, bool skip_cancel_fee = false);
|
||||
|
||||
/**
|
||||
* @brief Process a new limit order through the markets
|
||||
|
|
@ -418,27 +418,28 @@ namespace graphene { namespace chain {
|
|||
* This function takes a new limit order, and runs the markets attempting to match it with existing orders
|
||||
* already on the books.
|
||||
*/
|
||||
bool apply_order_before_hardfork_625(const limit_order_object& new_order_object, bool allow_black_swan = true);
|
||||
bool apply_order(const limit_order_object& new_order_object, bool allow_black_swan = true);
|
||||
|
||||
/**
|
||||
* Matches the two orders,
|
||||
* Matches the two orders, the first parameter is taker, the second is maker.
|
||||
*
|
||||
* @return a bit field indicating which orders were filled (and thus removed)
|
||||
*
|
||||
* 0 - no orders were matched
|
||||
* 1 - bid was filled
|
||||
* 2 - ask was filled
|
||||
* 1 - taker was filled
|
||||
* 2 - maker was filled
|
||||
* 3 - both were filled
|
||||
*/
|
||||
///@{
|
||||
template<typename OrderType>
|
||||
int match( const limit_order_object& bid, const OrderType& ask, const price& match_price );
|
||||
int match( const limit_order_object& bid, const limit_order_object& ask, const price& trade_price );
|
||||
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 );
|
||||
/// @return the amount of asset settled
|
||||
asset match(const call_order_object& call,
|
||||
const force_settlement_object& settle,
|
||||
const price& match_price,
|
||||
asset max_settlement);
|
||||
asset max_settlement,
|
||||
const price& fill_price);
|
||||
///@}
|
||||
|
||||
//////////////////// db_bet.cpp ////////////////////
|
||||
|
|
@ -467,9 +468,12 @@ namespace graphene { namespace chain {
|
|||
/**
|
||||
* @return true if the order was completely filled and thus freed.
|
||||
*/
|
||||
bool fill_order( const limit_order_object& order, const asset& pays, const asset& receives, bool cull_if_small );
|
||||
bool fill_order( const call_order_object& order, const asset& pays, const asset& receives );
|
||||
bool fill_order( const force_settlement_object& settle, const asset& pays, const asset& receives );
|
||||
bool fill_limit_order( const limit_order_object& order, const asset& pays, const asset& receives, bool cull_if_small,
|
||||
const price& fill_price, const bool is_maker );
|
||||
bool fill_call_order( const call_order_object& order, const asset& pays, const asset& receives,
|
||||
const price& fill_price, const bool is_maker );
|
||||
bool fill_settle_order( const force_settlement_object& settle, const asset& pays, const asset& receives,
|
||||
const price& fill_price, const bool is_maker );
|
||||
|
||||
bool check_call_orders( const asset_object& mia, bool enable_black_swan = true, bool for_new_limit_order = false,
|
||||
const asset_bitasset_data_object* bitasset_ptr = nullptr );
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ namespace graphene { namespace chain {
|
|||
*
|
||||
* Rather than returning a value, this method fills in core_fee_paid field.
|
||||
*/
|
||||
void convert_fee();
|
||||
virtual void convert_fee();
|
||||
|
||||
object_id_type get_relative_id( object_id_type rel_id )const;
|
||||
|
||||
|
|
|
|||
|
|
@ -45,12 +45,17 @@ namespace graphene { namespace chain {
|
|||
|
||||
asset calculate_market_fee( const asset_object* aobj, const asset& trade_amount );
|
||||
|
||||
/** override the default behavior defined by generic_evalautor
|
||||
*/
|
||||
virtual void convert_fee() override;
|
||||
|
||||
/** override the default behavior defined by generic_evalautor which is to
|
||||
* post the fee to fee_paying_account_stats.pending_fees
|
||||
*/
|
||||
virtual void pay_fee() override;
|
||||
|
||||
share_type _deferred_fee = 0;
|
||||
asset _deferred_paid_fee;
|
||||
const limit_order_create_operation* _op = nullptr;
|
||||
const account_object* _seller = nullptr;
|
||||
const asset_object* _sell_asset = nullptr;
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ class limit_order_object : public abstract_object<limit_order_object>
|
|||
account_id_type seller;
|
||||
share_type for_sale; ///< asset id is sell_price.base.asset_id
|
||||
price sell_price;
|
||||
share_type deferred_fee;
|
||||
share_type deferred_fee; ///< fee converted to CORE
|
||||
asset deferred_paid_fee; ///< originally paid fee
|
||||
|
||||
pair<asset_id_type,asset_id_type> get_market()const
|
||||
{
|
||||
|
|
@ -63,6 +64,8 @@ class limit_order_object : public abstract_object<limit_order_object>
|
|||
|
||||
asset amount_for_sale()const { return asset( for_sale, sell_price.base.asset_id ); }
|
||||
asset amount_to_receive()const { return amount_for_sale() * sell_price; }
|
||||
asset_id_type sell_asset_id()const { return sell_price.base.asset_id; }
|
||||
asset_id_type receive_asset_id()const { return sell_price.quote.asset_id; }
|
||||
};
|
||||
|
||||
struct by_id;
|
||||
|
|
@ -114,12 +117,13 @@ class call_order_object : public abstract_object<call_order_object>
|
|||
asset get_debt()const { return asset( debt, debt_type() ); }
|
||||
asset amount_to_receive()const { return get_debt(); }
|
||||
asset_id_type debt_type()const { return call_price.quote.asset_id; }
|
||||
asset_id_type collateral_type()const { return call_price.base.asset_id; }
|
||||
price collateralization()const { return get_collateral() / get_debt(); }
|
||||
|
||||
account_id_type borrower;
|
||||
share_type collateral; ///< call_price.base.asset_id, access via get_collateral
|
||||
share_type debt; ///< call_price.quote.asset_id, access via get_collateral
|
||||
price call_price; ///< Debt / Collateral
|
||||
share_type debt; ///< call_price.quote.asset_id, access via get_debt
|
||||
price call_price; ///< Collateral / Debt
|
||||
|
||||
pair<asset_id_type,asset_id_type> get_market()const
|
||||
{
|
||||
|
|
@ -207,7 +211,7 @@ typedef generic_index<force_settlement_object, force_settlement_object_multi_ind
|
|||
|
||||
FC_REFLECT_DERIVED( graphene::chain::limit_order_object,
|
||||
(graphene::db::object),
|
||||
(expiration)(seller)(for_sale)(sell_price)(deferred_fee)
|
||||
(expiration)(seller)(for_sale)(sell_price)(deferred_fee)(deferred_paid_fee)
|
||||
)
|
||||
|
||||
FC_REFLECT_DERIVED( graphene::chain::call_order_object, (graphene::db::object),
|
||||
|
|
|
|||
|
|
@ -136,14 +136,16 @@ namespace graphene { namespace chain {
|
|||
struct fee_parameters_type {};
|
||||
|
||||
fill_order_operation(){}
|
||||
fill_order_operation( object_id_type o, account_id_type a, asset p, asset r, asset f )
|
||||
:order_id(o),account_id(a),pays(p),receives(r),fee(f){}
|
||||
fill_order_operation( object_id_type o, account_id_type a, asset p, asset r, asset f, price fp, bool m )
|
||||
:order_id(o),account_id(a),pays(p),receives(r),fee(f),fill_price(fp),is_maker(m) {}
|
||||
|
||||
object_id_type order_id;
|
||||
account_id_type account_id;
|
||||
asset pays;
|
||||
asset receives;
|
||||
asset fee; // paid by receiving account
|
||||
price fill_price;
|
||||
bool is_maker;
|
||||
|
||||
|
||||
pair<asset_id_type,asset_id_type> get_market()const
|
||||
|
|
@ -169,7 +171,7 @@ FC_REFLECT( graphene::chain::fill_order_operation::fee_parameters_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) )
|
||||
FC_REFLECT( graphene::chain::fill_order_operation, (fee)(order_id)(account_id)(pays)(receives) )
|
||||
FC_REFLECT( graphene::chain::fill_order_operation, (fee)(order_id)(account_id)(pays)(receives)(fill_price)(is_maker) )
|
||||
|
||||
GRAPHENE_EXTERNAL_SERIALIZATION( extern, graphene::chain::limit_order_create_operation::fee_parameters_type )
|
||||
GRAPHENE_EXTERNAL_SERIALIZATION( extern, graphene::chain::limit_order_cancel_operation::fee_parameters_type )
|
||||
|
|
|
|||
|
|
@ -66,12 +66,32 @@ void_result limit_order_create_evaluator::do_evaluate(const limit_order_create_o
|
|||
return void_result();
|
||||
} FC_CAPTURE_AND_RETHROW( (op) ) }
|
||||
|
||||
void limit_order_create_evaluator::convert_fee()
|
||||
{
|
||||
if( db().head_block_time() <= HARDFORK_CORE_604_TIME )
|
||||
generic_evaluator::convert_fee();
|
||||
else
|
||||
if( !trx_state->skip_fee )
|
||||
{
|
||||
if( fee_asset->get_id() != asset_id_type() )
|
||||
{
|
||||
db().modify(*fee_asset_dyn_data, [this](asset_dynamic_data_object& d) {
|
||||
d.fee_pool -= core_fee_paid;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void limit_order_create_evaluator::pay_fee()
|
||||
{
|
||||
if( db().head_block_time() <= HARDFORK_445_TIME )
|
||||
generic_evaluator::pay_fee();
|
||||
else
|
||||
{
|
||||
_deferred_fee = core_fee_paid;
|
||||
if( db().head_block_time() > HARDFORK_CORE_604_TIME && fee_asset->get_id() != asset_id_type() )
|
||||
_deferred_paid_fee = fee_from_account;
|
||||
}
|
||||
}
|
||||
|
||||
object_id_type limit_order_create_evaluator::do_apply(const limit_order_create_operation& op)
|
||||
|
|
@ -92,9 +112,14 @@ object_id_type limit_order_create_evaluator::do_apply(const limit_order_create_o
|
|||
obj.sell_price = op.get_price();
|
||||
obj.expiration = op.expiration;
|
||||
obj.deferred_fee = _deferred_fee;
|
||||
obj.deferred_paid_fee = _deferred_paid_fee;
|
||||
});
|
||||
limit_order_id_type order_id = new_order_object.id; // save this because we may remove the object by filling it
|
||||
bool filled = db().apply_order(new_order_object);
|
||||
bool filled;
|
||||
if( db().get_dynamic_global_properties().next_maintenance_time <= HARDFORK_CORE_625_TIME )
|
||||
filled = db().apply_order_before_hardfork_625( new_order_object );
|
||||
else
|
||||
filled = db().apply_order( new_order_object );
|
||||
|
||||
FC_ASSERT( !op.fill_or_kill || filled );
|
||||
|
||||
|
|
@ -119,7 +144,7 @@ asset limit_order_cancel_evaluator::do_apply(const limit_order_cancel_operation&
|
|||
auto quote_asset = _order->sell_price.quote.asset_id;
|
||||
auto refunded = _order->amount_for_sale();
|
||||
|
||||
d.cancel_order(*_order, false /* don't create a virtual op*/);
|
||||
d.cancel_limit_order(*_order, false /* don't create a virtual op*/);
|
||||
|
||||
// Possible optimization: order can be called by canceling a limit order iff the canceled order was at the top of the book.
|
||||
// Do I need to check calls in both assets?
|
||||
|
|
|
|||
|
|
@ -106,10 +106,9 @@ struct operation_process_fill_order
|
|||
ho.time = time;
|
||||
ho.op = o;
|
||||
});
|
||||
|
||||
/*
|
||||
hkey.sequence += 200;
|
||||
itr = history_idx.lower_bound( hkey );
|
||||
/*
|
||||
while( itr != history_idx.end() )
|
||||
{
|
||||
if( itr->key.base == hkey.base && itr->key.quote == hkey.quote )
|
||||
|
|
@ -122,90 +121,105 @@ struct operation_process_fill_order
|
|||
*/
|
||||
|
||||
|
||||
/* Note: below is not true, because global settlement creates only one fill_order_op.
|
||||
* for every matched order there are two fill order operations created, one for
|
||||
* each side. We can filter the duplicates by only considering the fill operations where
|
||||
* the base > quote
|
||||
*/
|
||||
/*
|
||||
if( o.pays.asset_id > o.receives.asset_id )
|
||||
{
|
||||
//ilog( " skipping because base > quote" );
|
||||
return;
|
||||
}
|
||||
*/
|
||||
if( !o.is_maker )
|
||||
return;
|
||||
|
||||
bucket_key key;
|
||||
key.base = o.pays.asset_id;
|
||||
key.quote = o.receives.asset_id;
|
||||
|
||||
price trade_price = o.pays / o.receives;
|
||||
|
||||
|
||||
if( key.base > key.quote )
|
||||
{
|
||||
std::swap( key.base, key.quote );
|
||||
trade_price = ~trade_price;
|
||||
}
|
||||
|
||||
price fill_price = o.fill_price;
|
||||
if( fill_price.base.asset_id > fill_price.quote.asset_id )
|
||||
fill_price = ~fill_price;
|
||||
|
||||
auto max_history = _plugin.max_history();
|
||||
for( auto bucket : buckets )
|
||||
{
|
||||
auto cutoff = (fc::time_point() + fc::seconds( bucket * max_history));
|
||||
auto cutoff = (fc::time_point() + fc::seconds( bucket * max_history));
|
||||
|
||||
bucket_key key;
|
||||
key.base = o.pays.asset_id;
|
||||
key.quote = o.receives.asset_id;
|
||||
key.seconds = bucket;
|
||||
key.open = fc::time_point() + fc::seconds((_now.sec_since_epoch() / key.seconds) * key.seconds);
|
||||
|
||||
|
||||
/** for every matched order there are two fill order operations created, one for
|
||||
* each side. We can filter the duplicates by only considering the fill operations where
|
||||
* the base > quote
|
||||
*/
|
||||
if( key.base > key.quote )
|
||||
{
|
||||
//ilog( " skipping because base > quote" );
|
||||
continue;
|
||||
}
|
||||
|
||||
price trade_price = o.pays / o.receives;
|
||||
|
||||
key.seconds = bucket;
|
||||
key.open = fc::time_point() + fc::seconds((_now.sec_since_epoch() / key.seconds) * key.seconds);
|
||||
|
||||
const auto& by_key_idx = bucket_idx.indices().get<by_key>();
|
||||
auto itr = by_key_idx.find( key );
|
||||
if( itr == by_key_idx.end() )
|
||||
{ // create new bucket
|
||||
const auto& by_key_idx = bucket_idx.indices().get<by_key>();
|
||||
auto itr = by_key_idx.find( key );
|
||||
if( itr == by_key_idx.end() )
|
||||
{ // create new bucket
|
||||
/* const auto& obj = */
|
||||
db.create<bucket_object>( [&]( bucket_object& b ){
|
||||
b.key = key;
|
||||
b.quote_volume += trade_price.quote.amount;
|
||||
b.base_volume += trade_price.base.amount;
|
||||
b.open_base = trade_price.base.amount;
|
||||
b.open_quote = trade_price.quote.amount;
|
||||
b.close_base = trade_price.base.amount;
|
||||
b.close_quote = trade_price.quote.amount;
|
||||
b.high_base = b.close_base;
|
||||
b.high_quote = b.close_quote;
|
||||
b.low_base = b.close_base;
|
||||
b.low_quote = b.close_quote;
|
||||
});
|
||||
//wlog( " creating bucket ${b}", ("b",obj) );
|
||||
}
|
||||
else
|
||||
{ // update existing bucket
|
||||
//wlog( " before updating bucket ${b}", ("b",*itr) );
|
||||
db.modify( *itr, [&]( bucket_object& b ){
|
||||
b.base_volume += trade_price.base.amount;
|
||||
b.key = key;
|
||||
b.quote_volume += trade_price.quote.amount;
|
||||
b.close_base = trade_price.base.amount;
|
||||
b.close_quote = trade_price.quote.amount;
|
||||
if( b.high() < trade_price )
|
||||
{
|
||||
b.high_base = b.close_base;
|
||||
b.high_quote = b.close_quote;
|
||||
}
|
||||
if( b.low() > trade_price )
|
||||
{
|
||||
b.low_base = b.close_base;
|
||||
b.low_quote = b.close_quote;
|
||||
}
|
||||
});
|
||||
//wlog( " after bucket bucket ${b}", ("b",*itr) );
|
||||
}
|
||||
b.base_volume += trade_price.base.amount;
|
||||
b.open_base = fill_price.base.amount;
|
||||
b.open_quote = fill_price.quote.amount;
|
||||
b.close_base = fill_price.base.amount;
|
||||
b.close_quote = fill_price.quote.amount;
|
||||
b.high_base = b.close_base;
|
||||
b.high_quote = b.close_quote;
|
||||
b.low_base = b.close_base;
|
||||
b.low_quote = b.close_quote;
|
||||
});
|
||||
//wlog( " creating bucket ${b}", ("b",obj) );
|
||||
}
|
||||
else
|
||||
{ // update existing bucket
|
||||
//wlog( " before updating bucket ${b}", ("b",*itr) );
|
||||
db.modify( *itr, [&]( bucket_object& b ){
|
||||
b.base_volume += trade_price.base.amount;
|
||||
b.quote_volume += trade_price.quote.amount;
|
||||
b.close_base = fill_price.base.amount;
|
||||
b.close_quote = fill_price.quote.amount;
|
||||
if( b.high() < fill_price )
|
||||
{
|
||||
b.high_base = b.close_base;
|
||||
b.high_quote = b.close_quote;
|
||||
}
|
||||
if( b.low() > fill_price )
|
||||
{
|
||||
b.low_base = b.close_base;
|
||||
b.low_quote = b.close_quote;
|
||||
}
|
||||
});
|
||||
//wlog( " after bucket bucket ${b}", ("b",*itr) );
|
||||
}
|
||||
|
||||
if( max_history != 0 )
|
||||
{
|
||||
key.open = fc::time_point_sec();
|
||||
auto itr = by_key_idx.lower_bound( key );
|
||||
if( max_history != 0 )
|
||||
{
|
||||
key.open = fc::time_point_sec();
|
||||
auto itr = by_key_idx.lower_bound( key );
|
||||
|
||||
while( itr != by_key_idx.end() &&
|
||||
itr->key.base == key.base &&
|
||||
itr->key.quote == key.quote &&
|
||||
itr->key.seconds == bucket &&
|
||||
itr->key.open < cutoff )
|
||||
{
|
||||
// elog( " removing old bucket ${b}", ("b", *itr) );
|
||||
auto old_itr = itr;
|
||||
++itr;
|
||||
db.remove( *old_itr );
|
||||
}
|
||||
}
|
||||
while( itr != by_key_idx.end() &&
|
||||
itr->key.base == key.base &&
|
||||
itr->key.quote == key.quote &&
|
||||
itr->key.seconds == bucket &&
|
||||
itr->key.open < cutoff )
|
||||
{
|
||||
// elog( " removing old bucket ${b}", ("b", *itr) );
|
||||
auto old_itr = itr;
|
||||
++itr;
|
||||
db.remove( *old_itr );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -223,7 +237,12 @@ void market_history_plugin_impl::update_market_histories( const signed_block& b
|
|||
for( const optional< operation_history_object >& o_op : hist )
|
||||
{
|
||||
if( o_op.valid() )
|
||||
o_op->op.visit( operation_process_fill_order( _self, b.timestamp ) );
|
||||
{
|
||||
try
|
||||
{
|
||||
o_op->op.visit( operation_process_fill_order( _self, b.timestamp ) );
|
||||
} FC_CAPTURE_AND_LOG( (o_op) )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -857,21 +857,26 @@ digest_type database_fixture::digest( const transaction& tx )
|
|||
return tx.digest();
|
||||
}
|
||||
|
||||
const limit_order_object*database_fixture::create_sell_order(account_id_type user, const asset& amount, const asset& recv)
|
||||
const limit_order_object*database_fixture::create_sell_order(account_id_type user, const asset& amount, const asset& recv,
|
||||
const time_point_sec order_expiration,
|
||||
const price& fee_core_exchange_rate )
|
||||
{
|
||||
auto r = create_sell_order(user(db), amount, recv);
|
||||
auto r = create_sell_order(user(db), amount, recv, order_expiration, fee_core_exchange_rate);
|
||||
verify_asset_supplies(db);
|
||||
return r;
|
||||
}
|
||||
|
||||
const limit_order_object* database_fixture::create_sell_order( const account_object& user, const asset& amount, const asset& recv )
|
||||
const limit_order_object* database_fixture::create_sell_order( const account_object& user, const asset& amount, const asset& recv,
|
||||
const time_point_sec order_expiration,
|
||||
const price& fee_core_exchange_rate )
|
||||
{
|
||||
limit_order_create_operation buy_order;
|
||||
buy_order.seller = user.id;
|
||||
buy_order.amount_to_sell = amount;
|
||||
buy_order.min_to_receive = recv;
|
||||
buy_order.expiration = order_expiration;
|
||||
trx.operations.push_back(buy_order);
|
||||
for( auto& op : trx.operations ) db.current_fee_schedule().set_fee(op);
|
||||
for( auto& op : trx.operations ) db.current_fee_schedule().set_fee(op, fee_core_exchange_rate);
|
||||
trx.validate();
|
||||
auto processed = db.push_transaction(trx, ~0);
|
||||
trx.operations.clear();
|
||||
|
|
|
|||
|
|
@ -282,8 +282,12 @@ struct database_fixture {
|
|||
uint64_t fund( const account_object& account, const asset& amount = asset(500000) );
|
||||
digest_type digest( const transaction& tx );
|
||||
void sign( signed_transaction& trx, const fc::ecc::private_key& key );
|
||||
const limit_order_object* create_sell_order( account_id_type user, const asset& amount, const asset& recv );
|
||||
const limit_order_object* create_sell_order( const account_object& user, const asset& amount, const asset& recv );
|
||||
const limit_order_object* create_sell_order( account_id_type user, const asset& amount, const asset& recv,
|
||||
const time_point_sec order_expiration = time_point_sec::maximum(),
|
||||
const price& fee_core_exchange_rate = price::unit_price());
|
||||
const limit_order_object* create_sell_order( const account_object& user, const asset& amount, const asset& recv,
|
||||
const time_point_sec order_expiration = time_point_sec::maximum(),
|
||||
const price& fee_core_exchange_rate = price::unit_price());
|
||||
asset cancel_limit_order( const limit_order_object& order );
|
||||
void transfer( account_id_type from, account_id_type to, const asset& amount, const asset& fee = asset() );
|
||||
void transfer( const account_object& from, const account_object& to, const asset& amount, const asset& fee = asset() );
|
||||
|
|
|
|||
206
tests/tests/market_tests.cpp
Normal file
206
tests/tests/market_tests.cpp
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
#include <graphene/chain/hardfork.hpp>
|
||||
|
||||
#include <graphene/chain/protocol/market.hpp>
|
||||
#include <graphene/chain/market_object.hpp>
|
||||
|
||||
#include "../common/database_fixture.hpp"
|
||||
|
||||
using namespace graphene::chain;
|
||||
using namespace graphene::chain::test;
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE(market_tests, database_fixture)
|
||||
|
||||
/***
|
||||
* Reproduce bitshares-core issue #338 #343 #453 #606 #625 #649
|
||||
*/
|
||||
BOOST_AUTO_TEST_CASE(issue_338_etc)
|
||||
{ try {
|
||||
generate_blocks(HARDFORK_615_TIME); // get around Graphene issue #615 feed expiration bug
|
||||
generate_block();
|
||||
|
||||
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);
|
||||
asset_id_type usd_id = bitusd.id;
|
||||
asset_id_type core_id = core.id;
|
||||
|
||||
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
|
||||
const call_order_object& call = *borrow( borrower, bitusd.amount(1000), asset(15000));
|
||||
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
|
||||
const call_order_object& call2 = *borrow( borrower2, bitusd.amount(1000), asset(15500));
|
||||
call_order_id_type call2_id = call2.id;
|
||||
// create yet another position with 320% collateral, call price is 16/1.75 CORE/USD = 64/7
|
||||
const call_order_object& call3 = *borrow( borrower3, bitusd.amount(1000), asset(16000));
|
||||
call_order_id_type call3_id = call3.id;
|
||||
transfer(borrower, seller, bitusd.amount(1000));
|
||||
|
||||
BOOST_CHECK_EQUAL( 1000, call.debt.value );
|
||||
BOOST_CHECK_EQUAL( 15000, call.collateral.value );
|
||||
BOOST_CHECK_EQUAL( 1000, get_balance(seller, bitusd) );
|
||||
BOOST_CHECK_EQUAL( 0, get_balance(seller, core) );
|
||||
|
||||
// adjust price feed to get call_order 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 order slightly below the call price will not be matched #606
|
||||
limit_order_id_type sell_low = create_sell_order(seller, bitusd.amount(7), core.amount(59))->id;
|
||||
// This order above the MSSP will not be matched
|
||||
limit_order_id_type sell_high = create_sell_order(seller, bitusd.amount(7), core.amount(78))->id;
|
||||
// This would match but is blocked by sell_low?! #606
|
||||
limit_order_id_type sell_med = create_sell_order(seller, bitusd.amount(7), core.amount(60))->id;
|
||||
|
||||
cancel_limit_order( sell_med(db) );
|
||||
cancel_limit_order( sell_high(db) );
|
||||
cancel_limit_order( sell_low(db) );
|
||||
|
||||
// current implementation: an incoming limit order will be filled at the
|
||||
// requested price #338
|
||||
BOOST_CHECK( !create_sell_order(seller, bitusd.amount(7), core.amount(60)) );
|
||||
BOOST_CHECK_EQUAL( 993, get_balance(seller, bitusd) );
|
||||
BOOST_CHECK_EQUAL( 60, get_balance(seller, core) );
|
||||
BOOST_CHECK_EQUAL( 993, call.debt.value );
|
||||
BOOST_CHECK_EQUAL( 14940, call.collateral.value );
|
||||
|
||||
limit_order_id_type buy_low = create_sell_order(buyer, asset(90), bitusd.amount(10))->id;
|
||||
// margin call takes precedence
|
||||
BOOST_CHECK( !create_sell_order(seller, bitusd.amount(7), core.amount(60)) );
|
||||
BOOST_CHECK_EQUAL( 986, get_balance(seller, bitusd) );
|
||||
BOOST_CHECK_EQUAL( 120, get_balance(seller, core) );
|
||||
BOOST_CHECK_EQUAL( 986, call.debt.value );
|
||||
BOOST_CHECK_EQUAL( 14880, call.collateral.value );
|
||||
|
||||
limit_order_id_type buy_med = create_sell_order(buyer, asset(105), bitusd.amount(10))->id;
|
||||
// margin call takes precedence
|
||||
BOOST_CHECK( !create_sell_order(seller, bitusd.amount(7), core.amount(70)) );
|
||||
BOOST_CHECK_EQUAL( 979, get_balance(seller, bitusd) );
|
||||
BOOST_CHECK_EQUAL( 190, get_balance(seller, core) );
|
||||
BOOST_CHECK_EQUAL( 979, call.debt.value );
|
||||
BOOST_CHECK_EQUAL( 14810, call.collateral.value );
|
||||
|
||||
limit_order_id_type buy_high = create_sell_order(buyer, asset(115), bitusd.amount(10))->id;
|
||||
// margin call still has precedence (!) #625
|
||||
BOOST_CHECK( !create_sell_order(seller, bitusd.amount(7), core.amount(77)) );
|
||||
BOOST_CHECK_EQUAL( 972, get_balance(seller, bitusd) );
|
||||
BOOST_CHECK_EQUAL( 267, get_balance(seller, core) );
|
||||
BOOST_CHECK_EQUAL( 972, call.debt.value );
|
||||
BOOST_CHECK_EQUAL( 14733, call.collateral.value );
|
||||
|
||||
cancel_limit_order( buy_high(db) );
|
||||
cancel_limit_order( buy_med(db) );
|
||||
cancel_limit_order( buy_low(db) );
|
||||
|
||||
// call with more usd
|
||||
BOOST_CHECK( !create_sell_order(seller, bitusd.amount(700), core.amount(7700)) );
|
||||
BOOST_CHECK_EQUAL( 272, get_balance(seller, bitusd) );
|
||||
BOOST_CHECK_EQUAL( 7967, get_balance(seller, core) );
|
||||
BOOST_CHECK_EQUAL( 272, call.debt.value );
|
||||
BOOST_CHECK_EQUAL( 7033, call.collateral.value );
|
||||
|
||||
// at this moment, collateralization of call is 7033 / 272 = 25.8
|
||||
// collateralization of call2 is 15500 / 1000 = 15.5
|
||||
// collateralization of call3 is 16000 / 1000 = 16
|
||||
|
||||
// call more, still matches with the first call order #343
|
||||
BOOST_CHECK( !create_sell_order(seller, bitusd.amount(10), core.amount(110)) );
|
||||
BOOST_CHECK_EQUAL( 262, get_balance(seller, bitusd) );
|
||||
BOOST_CHECK_EQUAL( 8077, get_balance(seller, core) );
|
||||
BOOST_CHECK_EQUAL( 262, call.debt.value );
|
||||
BOOST_CHECK_EQUAL( 6923, call.collateral.value );
|
||||
|
||||
// at this moment, collateralization of call is 6923 / 262 = 26.4
|
||||
// collateralization of call2 is 15500 / 1000 = 15.5
|
||||
// collateralization of call3 is 16000 / 1000 = 16
|
||||
|
||||
// force settle
|
||||
force_settle( seller, bitusd.amount(10) );
|
||||
BOOST_CHECK_EQUAL( 252, get_balance(seller, bitusd) );
|
||||
BOOST_CHECK_EQUAL( 8077, get_balance(seller, core) );
|
||||
BOOST_CHECK_EQUAL( 262, call.debt.value );
|
||||
BOOST_CHECK_EQUAL( 6923, call.collateral.value );
|
||||
|
||||
// generate blocks to let the settle order execute (price feed will expire after it)
|
||||
generate_blocks( HARDFORK_615_TIME + fc::hours(25) );
|
||||
// call2 get settled #343
|
||||
BOOST_CHECK_EQUAL( 252, get_balance(seller_id, usd_id) );
|
||||
BOOST_CHECK_EQUAL( 8177, get_balance(seller_id, core_id) );
|
||||
BOOST_CHECK_EQUAL( 262, call_id(db).debt.value );
|
||||
BOOST_CHECK_EQUAL( 6923, call_id(db).collateral.value );
|
||||
BOOST_CHECK_EQUAL( 990, call2_id(db).debt.value );
|
||||
BOOST_CHECK_EQUAL( 15400, call2_id(db).collateral.value );
|
||||
|
||||
set_expiration( db, trx );
|
||||
update_feed_producers( usd_id(db), {feedproducer_id} );
|
||||
|
||||
// at this moment, collateralization of call is 8177 / 252 = 32.4
|
||||
// collateralization of call2 is 15400 / 990 = 15.5
|
||||
// collateralization of call3 is 16000 / 1000 = 16
|
||||
|
||||
// adjust price feed to get call2 into black swan territory, but not the first call order
|
||||
current_feed.settlement_price = asset(1, usd_id) / asset(20, core_id);
|
||||
publish_feed( usd_id(db), feedproducer_id(db), current_feed );
|
||||
// settlement price = 1/20, mssp = 1/22
|
||||
|
||||
// black swan event doesn't occur #649
|
||||
BOOST_CHECK( !usd_id(db).bitasset_data(db).has_settlement() );
|
||||
|
||||
// generate a block
|
||||
generate_block();
|
||||
|
||||
set_expiration( db, trx );
|
||||
update_feed_producers( usd_id(db), {feedproducer_id} );
|
||||
|
||||
// adjust price feed back
|
||||
current_feed.settlement_price = asset(1, usd_id) / asset(10, core_id);
|
||||
publish_feed( usd_id(db), feedproducer_id(db), current_feed );
|
||||
// settlement price = 1/10, mssp = 1/11
|
||||
|
||||
transfer(borrower2_id, seller_id, asset(1000, usd_id));
|
||||
transfer(borrower3_id, seller_id, asset(1000, usd_id));
|
||||
|
||||
// Re-create sell_low, slightly below the call price, will not be matched, will expire soon
|
||||
sell_low = create_sell_order(seller_id(db), asset(7, usd_id), asset(59), db.head_block_time()+fc::seconds(300) )->id;
|
||||
// This would match but is blocked by sell_low, it has an amount same as call's debt which will be full filled later
|
||||
sell_med = create_sell_order(seller_id(db), asset(262, usd_id), asset(2620))->id; // 1/10
|
||||
// Another big order above sell_med, blocked
|
||||
limit_order_id_type sell_med2 = create_sell_order(seller_id(db), asset(1200, usd_id), asset(12120))->id; // 1/10.1
|
||||
// Another small order above sell_med2, blocked
|
||||
limit_order_id_type sell_med3 = create_sell_order(seller_id(db), asset(120, usd_id), asset(1224))->id; // 1/10.2
|
||||
|
||||
// generate a block, sell_low will expire
|
||||
BOOST_TEST_MESSAGE( "Expire sell_low" );
|
||||
generate_blocks( HARDFORK_615_TIME + fc::hours(26) );
|
||||
BOOST_CHECK( db.find<limit_order_object>( sell_low ) == nullptr );
|
||||
|
||||
// #453 multiple order matching issue occurs
|
||||
BOOST_CHECK( db.find<limit_order_object>( sell_med ) == nullptr ); // sell_med get filled
|
||||
BOOST_CHECK( db.find<limit_order_object>( sell_med2 ) != nullptr ); // sell_med2 is still there
|
||||
BOOST_CHECK( db.find<limit_order_object>( sell_med3 ) == nullptr ); // sell_med3 get filled
|
||||
BOOST_CHECK( db.find<call_order_object>( call_id ) == nullptr ); // the first call order get filled
|
||||
BOOST_CHECK( db.find<call_order_object>( call2_id ) == nullptr ); // the second call order get filled
|
||||
BOOST_CHECK( db.find<call_order_object>( call3_id ) != nullptr ); // the third call order is still there
|
||||
|
||||
|
||||
} FC_LOG_AND_RETHROW() }
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
Loading…
Reference in a new issue