BitShares-Core  7.0.2
BitShares blockchain node software and command-line wallet software
db_update.cpp
Go to the documentation of this file.
1 /*
2  * Copyright (c) 2015 Cryptonomex, Inc., and contributors.
3  *
4  * The MIT License
5  *
6  * Permission is hereby granted, free of charge, to any person obtaining a copy
7  * of this software and associated documentation files (the "Software"), to deal
8  * in the Software without restriction, including without limitation the rights
9  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10  * copies of the Software, and to permit persons to whom the Software is
11  * furnished to do so, subject to the following conditions:
12  *
13  * The above copyright notice and this permission notice shall be included in
14  * all copies or substantial portions of the Software.
15  *
16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22  * THE SOFTWARE.
23  */
24 
27 
31 #include <graphene/chain/hardfork.hpp>
39 
41 
42 namespace graphene { namespace chain {
43 
44 void database::update_global_dynamic_data( const signed_block& b, const uint32_t missed_blocks )
45 {
46  const dynamic_global_property_object& _dgp = get_dynamic_global_properties();
47 
48  // dynamic global properties updating
49  modify( _dgp, [&b,this,missed_blocks]( dynamic_global_property_object& dgp ){
50  const uint32_t block_num = b.block_num();
51  if( BOOST_UNLIKELY( block_num == 1 ) )
52  dgp.recently_missed_count = 0;
53  else if( !_checkpoints.empty() && _checkpoints.rbegin()->first >= block_num )
54  dgp.recently_missed_count = 0;
55  else if( missed_blocks )
56  dgp.recently_missed_count += GRAPHENE_RECENTLY_MISSED_COUNT_INCREMENT*missed_blocks;
57  else if( dgp.recently_missed_count > GRAPHENE_RECENTLY_MISSED_COUNT_INCREMENT )
58  dgp.recently_missed_count -= GRAPHENE_RECENTLY_MISSED_COUNT_DECREMENT;
59  else if( dgp.recently_missed_count > 0 )
60  dgp.recently_missed_count--;
61 
62  dgp.head_block_number = block_num;
63  dgp.head_block_id = b.id();
64  dgp.time = b.timestamp;
65  dgp.current_witness = b.witness;
66  dgp.recent_slots_filled = (
67  (dgp.recent_slots_filled << 1)
68  + 1) << missed_blocks;
69  dgp.current_aslot += missed_blocks+1;
70  });
71 
72  if( 0 == (get_node_properties().skip_flags & skip_undo_history_check) )
73  {
74  GRAPHENE_ASSERT( _dgp.head_block_number - _dgp.last_irreversible_block_num < GRAPHENE_MAX_UNDO_HISTORY, undo_database_exception,
75  "The database does not have enough undo history to support a blockchain with so many missed blocks. "
76  "Please add a checkpoint if you would like to continue applying blocks beyond this point.",
77  ("last_irreversible_block_num",_dgp.last_irreversible_block_num)("head", _dgp.head_block_number)
78  ("recently_missed",_dgp.recently_missed_count)("max_undo",GRAPHENE_MAX_UNDO_HISTORY) );
79  }
80 
81  _undo_db.set_max_size( _dgp.head_block_number - _dgp.last_irreversible_block_num + 1 );
82  _fork_db.set_max_size( _dgp.head_block_number - _dgp.last_irreversible_block_num + 1 );
83 }
84 
85 void database::update_signing_witness(const witness_object& signing_witness, const signed_block& new_block)
86 {
87  const global_property_object& gpo = get_global_properties();
88  const dynamic_global_property_object& dpo = get_dynamic_global_properties();
89  uint64_t new_block_aslot = dpo.current_aslot + get_slot_at_time( new_block.timestamp );
90 
91  share_type witness_pay = std::min( gpo.parameters.witness_pay_per_block, dpo.witness_budget );
92 
93  modify( dpo, [&]( dynamic_global_property_object& _dpo )
94  {
95  _dpo.witness_budget -= witness_pay;
96  } );
97 
98  deposit_witness_pay( signing_witness, witness_pay );
99 
100  modify( signing_witness, [&]( witness_object& _wit )
101  {
102  _wit.last_aslot = new_block_aslot;
103  _wit.last_confirmed_block_num = new_block.block_num();
104  } );
105 }
106 
107 void database::update_last_irreversible_block()
108 {
109  const global_property_object& gpo = get_global_properties();
110  const dynamic_global_property_object& dpo = get_dynamic_global_properties();
111 
112  // TODO for better performance, move this to db_maint, because only need to do it once per maintenance interval
113  vector< const witness_object* > wit_objs;
114  wit_objs.reserve( gpo.active_witnesses.size() );
115  for( const witness_id_type& wid : gpo.active_witnesses )
116  wit_objs.push_back( &(wid(*this)) );
117 
118  static_assert( GRAPHENE_IRREVERSIBLE_THRESHOLD > 0, "irreversible threshold must be nonzero" );
119 
120  // 1 1 1 2 2 2 2 2 2 2 -> 2 .3*10 = 3
121  // 1 1 1 1 1 1 1 2 2 2 -> 1
122  // 3 3 3 3 3 3 3 3 3 3 -> 3
123  // 3 3 3 4 4 4 4 4 4 4 -> 4
124 
125  size_t offset = ((GRAPHENE_100_PERCENT - GRAPHENE_IRREVERSIBLE_THRESHOLD) * wit_objs.size() / GRAPHENE_100_PERCENT);
126 
127  std::nth_element( wit_objs.begin(), wit_objs.begin() + offset, wit_objs.end(),
128  []( const witness_object* a, const witness_object* b )
129  {
130  return a->last_confirmed_block_num < b->last_confirmed_block_num;
131  } );
132 
133  uint32_t new_last_irreversible_block_num = wit_objs[offset]->last_confirmed_block_num;
134 
135  if( new_last_irreversible_block_num > dpo.last_irreversible_block_num )
136  {
137  modify( dpo, [&]( dynamic_global_property_object& _dpo )
138  {
139  _dpo.last_irreversible_block_num = new_last_irreversible_block_num;
140  } );
141  }
142 }
143 
144 void database::clear_expired_transactions()
145 { try {
146  //Look for expired transactions in the deduplication list, and remove them.
147  //Transactions must have expired by at least two forking windows in order to be removed.
148  auto& transaction_idx = static_cast<transaction_index&>(get_mutable_index(implementation_ids,
149  impl_transaction_history_object_type));
150  const auto& dedupe_index = transaction_idx.indices().get<by_expiration>();
151  while( (!dedupe_index.empty()) && (head_block_time() > dedupe_index.begin()->trx.expiration) )
152  transaction_idx.remove(*dedupe_index.begin());
153 } FC_CAPTURE_AND_RETHROW() } // GCOVR_EXCL_LINE
154 
155 void database::clear_expired_proposals()
156 {
157  const auto& proposal_expiration_index = get_index_type<proposal_index>().indices().get<by_expiration>();
158  while( !proposal_expiration_index.empty() && proposal_expiration_index.begin()->expiration_time <= head_block_time() )
159  {
160  const proposal_object& proposal = *proposal_expiration_index.begin();
161  processed_transaction result;
162  try {
163  if( proposal.is_authorized_to_execute(*this) )
164  {
165  result = push_proposal(proposal);
166  //TODO: Do something with result so plugins can process it.
167  continue;
168  }
169  } catch( const fc::exception& e ) {
170  elog("Failed to apply proposed transaction on its expiration. Deleting it.\n${proposal}\n${error}",
171  ("proposal", proposal)("error", e.to_detail_string()));
172  }
173  remove(proposal);
174  }
175 }
176 
177 // Helper function to check whether we need to udpate current_feed.settlement_price.
178 static optional<price> get_derived_current_feed_price( const database& db,
179  const asset_bitasset_data_object& bitasset )
180 {
181  optional<price> result;
182  // check for null first
183  if( bitasset.median_feed.settlement_price.is_null() )
184  {
185  if( bitasset.current_feed.settlement_price.is_null() )
186  return result;
187  else
188  return bitasset.median_feed.settlement_price;
189  }
190 
192  const auto bsrm = bitasset.get_black_swan_response_method();
193  if( bsrm_type::no_settlement == bsrm )
194  {
195  // Find the call order with the least collateral ratio
196  const call_order_object* call_ptr = db.find_least_collateralized_short( bitasset, true );
197  if( call_ptr )
198  {
199  // GS if : call_ptr->collateralization() < ~( bitasset.median_feed.max_short_squeeze_price() )
200  auto least_collateral = call_ptr->collateralization();
201  auto lowest_callable_feed_price = (~least_collateral) / ratio_type( GRAPHENE_COLLATERAL_RATIO_DENOM,
202  bitasset.current_feed.maximum_short_squeeze_ratio );
203  result = std::max( bitasset.median_feed.settlement_price, lowest_callable_feed_price );
204  }
205  else // there is no call order of this bitasset
206  result = bitasset.median_feed.settlement_price;
207  }
208  else if( bsrm_type::individual_settlement_to_fund == bsrm && bitasset.individual_settlement_debt > 0 )
209  {
210  // Check whether to cap
211  price fund_price = asset( bitasset.individual_settlement_debt, bitasset.asset_id )
212  / asset( bitasset.individual_settlement_fund, bitasset.options.short_backing_asset );
213  auto lowest_callable_feed_price = fund_price * bitasset.get_margin_call_order_ratio();
214  result = std::max( bitasset.median_feed.settlement_price, lowest_callable_feed_price );
215  }
216  else // should not cap
217  result = bitasset.median_feed.settlement_price;
218 
219  // Check whether it's necessary to update
220  if( result.valid() && (*result) == bitasset.current_feed.settlement_price )
221  result.reset();
222  return result;
223 }
224 
225 // Helper function to update the limit order which is the individual settlement fund of the specified asset
226 static void update_settled_debt_order( database& db, const asset_bitasset_data_object& bitasset )
227 {
228  // To avoid unexpected price fluctuations, do not update the order if no sufficient price feeds
229  if( bitasset.current_feed.settlement_price.is_null() )
230  return;
231 
232  const limit_order_object* limit_ptr = db.find_settled_debt_order( bitasset.asset_id );
233  if( !limit_ptr )
234  return;
235 
236  bool sell_all = true;
237  share_type for_sale;
238 
239  // Note: bitasset.get_margin_call_order_price() is in debt/collateral
240  price sell_price = ~bitasset.get_margin_call_order_price();
241  asset settled_debt( bitasset.individual_settlement_debt, limit_ptr->receive_asset_id() );
242  try
243  {
244  for_sale = settled_debt.multiply_and_round_up( sell_price ).amount; // may overflow
245  if( for_sale <= bitasset.individual_settlement_fund ) // "=" is for the consistency of order matching logic
246  sell_all = false;
247  }
248  catch( const fc::exception& e ) // catch the overflow
249  {
250  // do nothing
251  dlog( e.to_detail_string() );
252  }
253 
254  // TODO Potential optimization: to avoid unnecessary database update, check before update
255  db.modify( *limit_ptr, [sell_all, &sell_price, &for_sale, &bitasset]( limit_order_object& obj )
256  {
257  if( sell_all )
258  {
259  obj.for_sale = bitasset.individual_settlement_fund;
260  obj.sell_price = ~bitasset.get_individual_settlement_price();
261  }
262  else
263  {
264  obj.for_sale = for_sale;
265  obj.sell_price = sell_price;
266  }
267  } );
268 }
269 
270 void database::update_bitasset_current_feed( const asset_bitasset_data_object& bitasset, bool skip_median_update )
271 {
272  // For better performance, if nothing to update, we return
273  optional<price> new_current_feed_price;
275  const auto bsrm = bitasset.get_black_swan_response_method();
276  if( skip_median_update )
277  {
278  if( bsrm_type::no_settlement != bsrm && bsrm_type::individual_settlement_to_fund != bsrm )
279  {
280  // it's possible that current_feed was capped thus we still need to update it
281  if( bitasset.current_feed.settlement_price == bitasset.median_feed.settlement_price )
282  return;
283  new_current_feed_price = bitasset.median_feed.settlement_price;
284  }
285  else
286  {
287  new_current_feed_price = get_derived_current_feed_price( *this, bitasset );
288  if( !new_current_feed_price.valid() )
289  return;
290  }
291  }
292 
293  const auto& head_time = head_block_time();
294 
295  // We need to update the database
296  modify( bitasset, [this, skip_median_update, &head_time, &new_current_feed_price, &bsrm]
298  {
299  if( !skip_median_update )
300  {
301  const auto& maint_time = get_dynamic_global_properties().next_maintenance_time;
302  abdo.update_median_feeds( head_time, maint_time );
303  abdo.current_feed = abdo.median_feed;
304  if( bsrm_type::no_settlement == bsrm || bsrm_type::individual_settlement_to_fund == bsrm )
305  new_current_feed_price = get_derived_current_feed_price( *this, abdo );
306  }
307  if( new_current_feed_price.valid() )
308  abdo.current_feed.settlement_price = *new_current_feed_price;
309  } );
310 
311  // Update individual settlement order price
312  if( !skip_median_update
313  && bsrm_type::individual_settlement_to_order == bsrm
314  && HARDFORK_CORE_2591_PASSED( head_time ) ) // Tighter peg (fill individual settlement order at MCOP)
315  {
316  update_settled_debt_order( *this, bitasset );
317  }
318 }
319 
320 void database::clear_expired_orders()
321 { try {
322  //Cancel expired limit orders
323  auto head_time = head_block_time();
325 
326  bool before_core_hardfork_606 = ( maint_time <= HARDFORK_CORE_606_TIME ); // feed always trigger call
327 
328  auto& limit_index = get_index_type<limit_order_index>().indices().get<by_expiration>();
329  while( !limit_index.empty() && limit_index.begin()->expiration <= head_time )
330  {
331  const limit_order_object& order = *limit_index.begin();
332  auto base_asset = order.sell_price.base.asset_id;
333  auto quote_asset = order.sell_price.quote.asset_id;
334  cancel_limit_order( order );
335  if( before_core_hardfork_606 )
336  {
337  // check call orders
338  // Comments below are copied from limit_order_cancel_evaluator::do_apply(...)
339  // Possible optimization: order can be called by cancelling a limit order
340  // if the canceled order was at the top of the book.
341  // Do I need to check calls in both assets?
342  check_call_orders( base_asset( *this ) );
343  check_call_orders( quote_asset( *this ) );
344  }
345  }
346 } FC_CAPTURE_AND_RETHROW() } // GCOVR_EXCL_LINE
347 
348 void database::clear_expired_force_settlements()
349 { try {
350  // Process expired force settlement orders
351 
352  // TODO Possible performance optimization. Looping through all assets is not ideal.
353  // - One idea is to check time first, if any expired settlement found, check asset.
354  // However, due to max_settlement_volume, this does not work, i.e. time meets but have to
355  // skip due to volume limit.
356  // - Instead, maintain some data e.g. (whether_force_settle_volome_meets, first_settle_time)
357  // in bitasset_data object and index by them, then we could process here faster.
358  // Note: due to rounding, even when settled < max_volume, it is still possible that we have to skip
359  const auto& settlement_index = get_index_type<force_settlement_index>().indices().get<by_expiration>();
360  if( settlement_index.empty() )
361  return;
362 
363  const auto& head_time = head_block_time();
364  const auto& maint_time = get_dynamic_global_properties().next_maintenance_time;
365 
366  const bool before_core_hardfork_184 = ( maint_time <= HARDFORK_CORE_184_TIME ); // something-for-nothing
367  const bool before_core_hardfork_342 = ( maint_time <= HARDFORK_CORE_342_TIME ); // better rounding
368 
369  asset_id_type current_asset = settlement_index.begin()->settlement_asset_id();
370  const asset_object* mia_object_ptr = &get(current_asset);
371  const asset_bitasset_data_object* mia_ptr = &mia_object_ptr->bitasset_data(*this);
372 
373  asset max_settlement_volume;
374  price settlement_fill_price;
375  price settlement_price;
376  bool current_asset_finished = false;
377 
378  auto next_asset = [&current_asset, &mia_object_ptr, &mia_ptr, &current_asset_finished, &settlement_index, this] {
379  const auto bound = settlement_index.upper_bound(current_asset);
380  if( bound == settlement_index.end() )
381  return false;
382  current_asset = bound->settlement_asset_id();
383  mia_object_ptr = &get(current_asset);
384  mia_ptr = &mia_object_ptr->bitasset_data(*this);
385  current_asset_finished = false;
386  return true;
387  };
388 
389  // At each iteration, we either consume the current order and remove it, or we move to the next asset
390  for( auto itr = settlement_index.lower_bound(current_asset);
391  itr != settlement_index.end();
392  itr = settlement_index.lower_bound(current_asset) )
393  {
394  const force_settlement_object& settle_order = *itr;
395  auto settle_order_id = settle_order.id;
396 
397  if( current_asset != settle_order.settlement_asset_id() )
398  {
399  current_asset = settle_order.settlement_asset_id();
400  mia_object_ptr = &get(current_asset);
401  mia_ptr = &mia_object_ptr->bitasset_data(*this);
402  // Note: we did not reset current_asset_finished to false here, it is OK,
403  // because current_asset should not have changed if current_asset_finished is true
404  }
405  const asset_object& mia_object = *mia_object_ptr;
406  const asset_bitasset_data_object& mia = *mia_ptr;
407 
408  if( mia.is_globally_settled() )
409  {
410  ilog( "Canceling a force settlement because of black swan" );
411  cancel_settle_order( settle_order );
412  continue;
413  }
414 
415  // Has this order not reached its settlement date?
416  if( settle_order.settlement_date > head_time )
417  {
418  if( next_asset() )
419  continue;
420  break;
421  }
422  // Can we still settle in this asset?
423  if( mia.current_feed.settlement_price.is_null() )
424  {
425  ilog("Canceling a force settlement in ${asset} because settlement price is null",
426  ("asset", mia_object.symbol));
427  cancel_settle_order(settle_order);
428  continue;
429  }
430  if( GRAPHENE_100_PERCENT == mia.options.force_settlement_offset_percent ) // settle something for nothing
431  {
432  ilog( "Canceling a force settlement in ${asset} because settlement offset is 100%",
433  ("asset", mia_object.symbol));
434  cancel_settle_order(settle_order);
435  continue;
436  }
437  // Note: although current supply would decrease during filling the settle orders,
438  // we always calculate with the initial value
439  if( max_settlement_volume.asset_id != current_asset )
440  max_settlement_volume = mia_object.amount( mia.max_force_settlement_volume(
441  mia_object.dynamic_data(*this).current_supply ) );
442  // When current_asset_finished is true, this would be the 2nd time processing the same order.
443  // In this case, we move to the next asset.
444  if( mia.force_settled_volume >= max_settlement_volume.amount || current_asset_finished )
445  {
446  if( next_asset() )
447  continue;
448  break;
449  }
450 
451  if( settlement_fill_price.base.asset_id != current_asset ) // only calculate once per asset
452  settlement_fill_price = mia.current_feed.settlement_price
453  / ratio_type( GRAPHENE_100_PERCENT - mia.options.force_settlement_offset_percent,
455 
456  if( before_core_hardfork_342 )
457  {
458  auto& pays = settle_order.balance;
459  auto receives = (settle_order.balance * mia.current_feed.settlement_price);
460  receives.amount = static_cast<uint64_t>( ( fc::uint128_t(receives.amount.value) *
461  (GRAPHENE_100_PERCENT - mia.options.force_settlement_offset_percent) ) /
463  assert(receives <= settle_order.balance * mia.current_feed.settlement_price);
464  settlement_price = pays / receives;
465  }
466  else if( settlement_price.base.asset_id != current_asset ) // only calculate once per asset
467  settlement_price = settlement_fill_price;
468 
469  asset settled = mia_object.amount(mia.force_settled_volume);
470  // Match against the least collateralized short until the settlement is finished or we reach max settlements
471  while( settled < max_settlement_volume && find_object(settle_order_id) )
472  {
473  if( 0 == settle_order.balance.amount )
474  {
475  wlog( "0 settlement detected" );
476  cancel_settle_order( settle_order );
477  break;
478  }
479 
480  const call_order_object* call_ptr = find_least_collateralized_short( mia, true );
481  // Note: there can be no debt position due to individual settlements
482  if( !call_ptr ) // no debt position
483  {
484  wlog( "No debt position found when processing force settlement ${o}", ("o",settle_order) );
485  cancel_settle_order( settle_order );
486  break;
487  }
488 
489  try {
490  asset max_settlement = max_settlement_volume - settled;
491 
492  asset new_settled = match( settle_order, *call_ptr, settlement_price, mia,
493  max_settlement, settlement_fill_price );
494  if( !before_core_hardfork_184 && new_settled.amount == 0 ) // unable to fill this settle order
495  {
496  // current asset is finished when the settle order hasn't been cancelled
497  current_asset_finished = ( nullptr != find_object( settle_order_id ) );
498  break;
499  }
500  settled += new_settled;
501  // before hard fork core-342, `new_settled > 0` is always true, we'll have:
502  // * call order is completely filled (thus call_ptr will change in next loop), or
503  // * settle order is completely filled (thus find_object(settle_order_id) will be false so will
504  // break out), or
505  // * reached max_settlement_volume limit (thus new_settled == max_settlement so will break out).
506  //
507  // after hard fork core-342, if new_settled > 0, we'll have:
508  // * call order is completely filled (thus call_ptr will change in next loop), or
509  // * settle order is completely filled (thus find_object(settle_order_id) will be false so will
510  // break out), or
511  // * reached max_settlement_volume limit, but it's possible that new_settled < max_settlement,
512  // in this case, new_settled will be zero in next iteration of the loop, so no need to check here.
513  }
514  catch ( const black_swan_exception& e ) {
515  wlog( "Cancelling a settle_order since it may trigger a black swan: ${o}, ${e}",
516  ("o", settle_order)("e", e.to_detail_string()) );
517  cancel_settle_order( settle_order );
518  break;
519  }
520  }
521  if( mia.force_settled_volume != settled.amount )
522  {
523  modify(mia, [&settled](asset_bitasset_data_object& b) {
524  b.force_settled_volume = settled.amount;
525  });
526  }
527  }
528 } FC_CAPTURE_AND_RETHROW() } // GCOVR_EXCL_LINE
529 
530 void database::update_expired_feeds()
531 {
532  const auto head_time = head_block_time();
533  bool after_hardfork_615 = ( head_time >= HARDFORK_615_TIME );
534  bool after_core_hardfork_2582 = HARDFORK_CORE_2582_PASSED( head_time ); // Price feed issues
535 
536  const auto& idx = get_index_type<asset_bitasset_data_index>().indices().get<by_feed_expiration>();
537  auto itr = idx.begin();
538  while( itr != idx.end() && itr->feed_is_expired( head_time ) )
539  {
540  const asset_bitasset_data_object& b = *itr;
541  ++itr; // not always process begin() because old code skipped updating some assets before hf 615
542 
543  // update feeds, check margin calls
544  if( !( after_hardfork_615 || b.feed_is_expired_before_hf_615( head_time ) ) )
545  continue;
546 
547  auto old_current_feed = b.current_feed;
548  auto old_median_feed = b.median_feed;
549  const asset_object& asset_obj = b.asset_id( *this );
551  // Note: we don't try to revive the bitasset here if it was GSed // TODO probably we should do it
552 
553  if( !b.current_feed.settlement_price.is_null()
554  && !b.current_feed.margin_call_params_equal( old_current_feed ) )
555  {
556  check_call_orders( asset_obj, true, false, &b, true );
557  }
558  else if( after_core_hardfork_2582 && !b.median_feed.settlement_price.is_null()
559  && !b.median_feed.margin_call_params_equal( old_median_feed ) )
560  {
561  check_call_orders( asset_obj, true, false, &b, true );
562  }
563  // update CER
564  if( b.need_to_update_cer() )
565  {
566  modify( b, []( asset_bitasset_data_object& abdo )
567  {
568  abdo.asset_cer_updated = false;
569  abdo.feed_cer_updated = false;
570  });
571  if( asset_obj.options.core_exchange_rate != b.current_feed.core_exchange_rate )
572  {
573  modify( asset_obj, [&b]( asset_object& ao )
574  {
575  ao.options.core_exchange_rate = b.current_feed.core_exchange_rate;
576  });
577  }
578  }
579  } // for each asset whose feed is expired
580 
581  // process assets affected by bitshares-core issue 453 before hard fork 615
582  if( !after_hardfork_615 )
583  {
584  for( asset_id_type a : _issue_453_affected_assets )
585  {
586  check_call_orders( a(*this) );
587  }
588  }
589 }
590 
591 void database::update_core_exchange_rates()
592 {
593  const auto& idx = get_index_type<asset_bitasset_data_index>().indices().get<by_cer_update>();
594  if( idx.begin() != idx.end() )
595  {
596  for( auto itr = idx.rbegin(); itr->need_to_update_cer(); itr = idx.rbegin() )
597  {
598  const asset_bitasset_data_object& b = *itr;
599  const asset_object& a = b.asset_id( *this );
600  if( a.options.core_exchange_rate != b.current_feed.core_exchange_rate )
601  {
602  modify( a, [&b]( asset_object& ao )
603  {
604  ao.options.core_exchange_rate = b.current_feed.core_exchange_rate;
605  });
606  }
607  modify( b, []( asset_bitasset_data_object& abdo )
608  {
609  abdo.asset_cer_updated = false;
610  abdo.feed_cer_updated = false;
611  });
612  }
613  }
614 }
615 
616 void database::update_maintenance_flag( bool new_maintenance_flag )
617 {
618  modify( get_dynamic_global_properties(), [&]( dynamic_global_property_object& dpo )
619  {
620  auto maintenance_flag = dynamic_global_property_object::maintenance_flag;
621  dpo.dynamic_flags =
622  (dpo.dynamic_flags & (uint32_t)(~maintenance_flag))
623  | (new_maintenance_flag ? (uint32_t)maintenance_flag : 0U);
624  } );
625  return;
626 }
627 
628 void database::update_withdraw_permissions()
629 {
630  auto& permit_index = get_index_type<withdraw_permission_index>().indices().get<by_expiration>();
631  while( !permit_index.empty() && permit_index.begin()->expiration <= head_block_time() )
632  remove(*permit_index.begin());
633 }
634 
635 void database::clear_expired_htlcs()
636 {
637  const auto& htlc_idx = get_index_type<htlc_index>().indices().get<by_expiration>();
638  while ( htlc_idx.begin() != htlc_idx.end()
639  && htlc_idx.begin()->conditions.time_lock.expiration <= head_block_time() )
640  {
641  const htlc_object& obj = *htlc_idx.begin();
642  const auto amount = asset(obj.transfer.amount, obj.transfer.asset_id);
643  adjust_balance( obj.transfer.from, amount );
644  // notify related parties
645  htlc_refund_operation vop( obj.get_id(), obj.transfer.from, obj.transfer.to, amount,
646  obj.conditions.hash_lock.preimage_hash, obj.conditions.hash_lock.preimage_size );
647  push_applied_operation( vop );
648  remove( obj );
649  }
650 }
651 
653 {
654  const auto maint_time = get_dynamic_global_properties().next_maintenance_time;
655  ticket_version version = ( HARDFORK_CORE_2262_PASSED(maint_time) ? ticket_v2 : ticket_v1 );
656 
658  share_type total_delta_pob;
659  share_type total_delta_inactive;
660  auto& idx = get_index_type<ticket_index>().indices().get<by_next_update>();
661  while( !idx.empty() && idx.begin()->next_auto_update_time <= head_block_time() )
662  {
663  const ticket_object& ticket = *idx.begin();
664  const auto& stat = get_account_stats_by_owner( ticket.account );
665  if( ticket.status == withdrawing && ticket.current_type == liquid )
666  {
667  adjust_balance( ticket.account, ticket.amount );
668  // Note: amount.asset_id is checked when creating the ticket, so no check here
669  modify( stat, [&ticket](account_statistics_object& aso) {
670  aso.total_core_pol -= ticket.amount.amount;
671  aso.total_pol_value -= ticket.value;
672  });
673  result.removed_objects.insert( ticket.id );
674  remove( ticket );
675  }
676  else
677  {
678  ticket_type old_type = ticket.current_type;
679  share_type old_value = ticket.value;
680  modify( ticket, [version]( ticket_object& o ) {
681  o.auto_update( version );
682  });
683  result.updated_objects.insert( ticket.id );
684 
685  share_type delta_inactive_amount;
686  share_type delta_forever_amount;
687  share_type delta_forever_value;
688  share_type delta_other_amount;
689  share_type delta_other_value;
690 
691  if( old_type == lock_forever ) // It implies that the new type is lock_forever too
692  {
693  if( ticket.value == 0 )
694  {
695  total_delta_pob -= ticket.amount.amount;
696  total_delta_inactive += ticket.amount.amount;
697  delta_inactive_amount = ticket.amount.amount;
698  delta_forever_amount = -ticket.amount.amount;
699  }
700  delta_forever_value = ticket.value - old_value;
701  }
702  else // old_type != lock_forever
703  {
704  if( ticket.current_type == lock_forever )
705  {
706  total_delta_pob += ticket.amount.amount;
707  delta_forever_amount = ticket.amount.amount;
708  delta_forever_value = ticket.value;
709  delta_other_amount = -ticket.amount.amount;
710  delta_other_value = -old_value;
711  }
712  else // ticket.current_type != lock_forever
713  {
714  delta_other_value = ticket.value - old_value;
715  }
716  }
717 
718  // Note: amount.asset_id is checked when creating the ticket, so no check here
719  modify( stat, [delta_inactive_amount,delta_forever_amount,delta_forever_value,
720  delta_other_amount,delta_other_value](account_statistics_object& aso) {
721  aso.total_core_inactive += delta_inactive_amount;
722  aso.total_core_pob += delta_forever_amount;
723  aso.total_core_pol += delta_other_amount;
724  aso.total_pob_value += delta_forever_value;
725  aso.total_pol_value += delta_other_value;
726  });
727 
728  }
729  // TODO if a lock_forever ticket lost all the value, remove it
730  }
731 
732  // TODO merge stable tickets with the same account and the same type
733 
734  // Update global data
735  if( total_delta_pob != 0 || total_delta_inactive != 0 )
736  {
738  [total_delta_pob,total_delta_inactive]( dynamic_global_property_object& dgp ) {
739  dgp.total_pob += total_delta_pob;
740  dgp.total_inactive += total_delta_inactive;
741  });
742  }
743 
744  return result;
745 }
746 
747 void database::update_credit_offers_and_deals()
748 {
749  const auto head_time = head_block_time();
750 
751  // Auto-disable offers
752  const auto& offer_idx = get_index_type<credit_offer_index>().indices().get<by_auto_disable_time>();
753  auto offer_itr = offer_idx.lower_bound( true );
754  auto offer_itr_end = offer_idx.upper_bound( boost::make_tuple( true, head_time ) );
755  while( offer_itr != offer_itr_end )
756  {
757  const credit_offer_object& offer = *offer_itr;
758  ++offer_itr;
759  modify( offer, []( credit_offer_object& obj ) {
760  obj.enabled = false;
761  });
762  }
763 
764  // Auto-process deals
765  const auto& deal_idx = get_index_type<credit_deal_index>().indices().get<by_latest_repay_time>();
766  const auto& deal_summary_idx = get_index_type<credit_deal_summary_index>().indices().get<by_offer_borrower>();
767  auto deal_itr_end = deal_idx.upper_bound( head_time );
768  for( auto deal_itr = deal_idx.begin(); deal_itr != deal_itr_end; deal_itr = deal_idx.begin() )
769  {
770  const credit_deal_object& deal = *deal_itr;
771 
772  // Process automatic repayment
773  // Note: an automatic repayment may fail, in which case we consider the credit deal past due without repayment
774  using repay_type = credit_deal_auto_repayment_type;
775  if( static_cast<uint8_t>(repay_type::no_auto_repayment) != deal.auto_repay )
776  {
777  credit_deal_repay_operation op;
778  op.account = deal.borrower;
779  op.deal_id = deal.get_id();
780  // Amounts
781  // Note: the result can be larger than 64 bit
782  auto required_fee = ( ( ( fc::uint128_t( deal.debt_amount.value ) * deal.fee_rate )
783  + GRAPHENE_FEE_RATE_DENOM ) - 1 ) / GRAPHENE_FEE_RATE_DENOM; // Round up
784  fc::uint128_t total_required = required_fee + deal.debt_amount.value;
785  auto balance = get_balance( deal.borrower, deal.debt_asset );
786  if( static_cast<uint8_t>(repay_type::only_full_repayment) == deal.auto_repay
787  || fc::uint128_t( balance.amount.value ) >= total_required )
788  { // if only full repayment or account balance is sufficient
789  op.repay_amount = asset( deal.debt_amount, deal.debt_asset );
790  op.credit_fee = asset( static_cast<int64_t>( required_fee ), deal.debt_asset );
791  }
792  else // Allow partial repayment
793  {
794  fc::uint128_t debt_to_repay = ( fc::uint128_t( balance.amount.value ) * GRAPHENE_FEE_RATE_DENOM )
795  / ( GRAPHENE_FEE_RATE_DENOM + deal.fee_rate ); // Round down
796  fc::uint128_t collateral_to_release = ( debt_to_repay * deal.collateral_amount.value )
797  / deal.debt_amount.value; // Round down
798  debt_to_repay = ( ( ( collateral_to_release * deal.debt_amount.value ) + deal.collateral_amount.value )
799  - 1 ) / deal.collateral_amount.value; // Round up
800  fc::uint128_t fee_to_pay = ( ( ( debt_to_repay * deal.fee_rate )
801  + GRAPHENE_FEE_RATE_DENOM ) - 1 ) / GRAPHENE_FEE_RATE_DENOM; // Round up
802  op.repay_amount = asset( static_cast<int64_t>( debt_to_repay ), deal.debt_asset );
803  op.credit_fee = asset( static_cast<int64_t>( fee_to_pay ), deal.debt_asset );
804  }
805 
806  auto deal_copy = deal; // Make a copy for logging
807 
808  transaction_evaluation_state eval_state(this);
809  eval_state.skip_fee_schedule_check = true;
810 
811  try
812  {
813  try_push_virtual_operation( eval_state, op );
814  }
815  catch( const fc::exception& e )
816  {
817  // We can in fact get here,
818  // e.g. if the debt asset issuer blacklisted the account, or account balance is insufficient
819  wlog( "Automatic repayment ${op} for credit deal ${credit_deal} failed at block ${n}; "
820  "account balance was ${balance}; exception was ${e}",
821  ("op", op)("credit_deal", deal_copy)
822  ("n", head_block_num())("balance", balance)("e", e.to_detail_string()) );
823  }
824 
825  if( !find( op.deal_id ) ) // The credit deal is fully repaid
826  continue;
827  }
828 
829  // Update offer
830  // Note: offer balance can be zero after updated. TODO remove zero-balance offers after a period
831  const credit_offer_object& offer = deal.offer_id(*this);
832  modify( offer, [&deal]( credit_offer_object& obj ){
833  obj.total_balance -= deal.debt_amount;
834  });
835 
836  // Process deal summary
837  auto summ_itr = deal_summary_idx.find( boost::make_tuple( deal.offer_id, deal.borrower ) );
838  if( summ_itr == deal_summary_idx.end() ) // This should not happen, just be defensive here
839  {
840  // We do not do FC_ASSERT or FC_THROW here to avoid halting the chain
841  elog( "Error: unable to find the credit deal summary object for credit deal ${d}",
842  ("d", deal) );
843  }
844  else
845  {
846  const credit_deal_summary_object& summ_obj = *summ_itr;
847  if( summ_obj.total_debt_amount == deal.debt_amount )
848  {
849  remove( summ_obj );
850  }
851  else
852  {
853  modify( summ_obj, [&deal]( credit_deal_summary_object& obj ){
854  obj.total_debt_amount -= deal.debt_amount;
855  });
856  }
857  }
858 
859  // Adjust balance
860  adjust_balance( deal.offer_owner, asset( deal.collateral_amount, deal.collateral_asset ) );
861 
862  // Notify related parties
863  push_applied_operation( credit_deal_expired_operation (
864  deal.get_id(), deal.offer_id, deal.offer_owner, deal.borrower,
865  asset( deal.debt_amount, deal.debt_asset ),
866  asset( deal.collateral_amount, deal.collateral_asset ),
867  deal.fee_rate ) );
868 
869  // Remove the deal
870  remove( deal );
871  }
872 }
873 
874 } }
graphene::db::object_database::find_object
const object * find_object(const object_id_type &id) const
Definition: object_database.cpp:43
FC_CAPTURE_AND_RETHROW
#define FC_CAPTURE_AND_RETHROW(...)
Definition: exception.hpp:479
GRAPHENE_COLLATERAL_RATIO_DENOM
#define GRAPHENE_COLLATERAL_RATIO_DENOM
Definition: config.hpp:113
graphene::db::object::id
object_id_type id
Definition: object.hpp:69
graphene::chain::limit_order_object::sell_price
price sell_price
The seller's asking price.
Definition: market_object.hpp:51
transaction_history_object.hpp
graphene::chain::dynamic_global_property_object
Maintains global state information (committee_member list, current fees)
Definition: global_property_object.hpp:62
graphene::chain::ticket_object::status
ticket_status status
The status of the ticket.
Definition: ticket_object.hpp:70
wlog
#define wlog(FORMAT,...)
Definition: logger.hpp:123
graphene::chain::asset_bitasset_data_object::update_median_feeds
void update_median_feeds(time_point_sec current_time, time_point_sec next_maintenance_time)
Definition: asset_object.cpp:47
fc::exception
Used to generate a useful error report when an exception is thrown.
Definition: exception.hpp:56
graphene::chain::database::head_block_time
time_point_sec head_block_time() const
Definition: db_getter.cpp:67
graphene::chain::credit_offer_object
A credit offer is a fund that can be used by other accounts who provide certain collateral.
Definition: credit_offer_object.hpp:39
graphene::chain::account_statistics_object::total_pol_value
share_type total_pol_value
Total value of tickets whose current type is not lock_forever.
Definition: account_object.hpp:83
asset_object.hpp
graphene::db::object_database::get
const T & get(const object_id_type &id) const
Definition: object_database.hpp:119
graphene::chain::database::get_balance
asset get_balance(account_id_type owner, asset_id_type asset_id) const
Retrieve a particular account's balance in a given asset.
Definition: db_balance.cpp:35
graphene::protocol::bitasset_options::black_swan_response_type
black_swan_response_type
Defines how a BitAsset would respond to black swan events.
Definition: asset_ops.hpp:112
database.hpp
graphene::protocol::liquid
@ liquid
Definition: ticket.hpp:35
graphene::chain::dynamic_global_property_object::next_maintenance_time
time_point_sec next_maintenance_time
Definition: global_property_object.hpp:70
graphene::chain::ticket_object
a ticket for governance voting
Definition: ticket_object.hpp:62
graphene::chain::fork_database::set_max_size
void set_max_size(uint32_t s)
Definition: fork_database.cpp:100
graphene::chain::ticket_version
ticket_version
Version of a ticket.
Definition: ticket_object.hpp:50
fee_schedule.hpp
graphene::protocol::ticket_type
ticket_type
Type of a ticket.
Definition: ticket.hpp:33
graphene::chain::database::adjust_balance
void adjust_balance(account_id_type account, asset delta)
Adjust a particular account's balance in a given asset by a delta.
Definition: db_balance.cpp:54
graphene::protocol::price_feed::settlement_price
price settlement_price
Definition: asset.hpp:180
graphene::chain::withdrawing
@ withdrawing
Definition: ticket_object.hpp:45
graphene::chain::database::check_call_orders
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, bool mute_exceptions=false, bool skip_matching_settle_orders=false)
Definition: db_market.cpp:2079
graphene::chain::database::push_proposal
processed_transaction push_proposal(const proposal_object &proposal)
Definition: db_block.cpp:336
proposal_object.hpp
graphene::chain::database::deposit_witness_pay
void deposit_witness_pay(const witness_object &wit, share_type amount)
helper to handle witness pay
Definition: db_balance.cpp:249
graphene::chain::ticket_object::value
share_type value
The current value of the ticket.
Definition: ticket_object.hpp:71
graphene::chain::credit_offer_object::enabled
bool enabled
Whether this offer is available.
Definition: credit_offer_object.hpp:49
graphene::chain::ticket_object::auto_update
void auto_update(ticket_version version)
Update the ticket when it's time.
Definition: ticket_object.cpp:101
graphene::chain::database::cancel_limit_order
void cancel_limit_order(const limit_order_object &order, bool create_virtual_op=true, bool skip_cancel_fee=false)
Definition: db_market.cpp:533
graphene::db::object_database::get_mutable_index
index & get_mutable_index()
Definition: object_database.hpp:179
graphene::db::object_database::find
const T * find(const object_id_type &id) const
Definition: object_database.hpp:126
graphene::chain::dynamic_global_property_object::total_inactive
share_type total_inactive
Definition: global_property_object.hpp:75
graphene::chain::account_statistics_object
Definition: account_object.hpp:46
ilog
#define ilog(FORMAT,...)
Definition: logger.hpp:117
graphene::chain::database::get_node_properties
const node_property_object & get_node_properties() const
Definition: db_getter.cpp:92
graphene::chain::ticket_v1
@ ticket_v1
Definition: ticket_object.hpp:52
dlog
#define dlog(FORMAT,...)
Definition: logger.hpp:100
graphene::chain::database::update_bitasset_current_feed
void update_bitasset_current_feed(const asset_bitasset_data_object &bitasset, bool skip_median_update=false)
Definition: db_update.cpp:270
fc::optional::valid
bool valid() const
Definition: optional.hpp:186
graphene::chain::account_statistics_object::total_core_inactive
share_type total_core_inactive
Total amount of core token in inactive lock_forever tickets.
Definition: account_object.hpp:71
graphene::chain::ticket_v2
@ ticket_v2
Definition: ticket_object.hpp:53
graphene::protocol::asset::asset_id
asset_id_type asset_id
Definition: asset.hpp:37
graphene::chain::database::push_applied_operation
uint32_t push_applied_operation(const operation &op, bool is_virtual=true)
Definition: db_block.cpp:548
graphene::chain::by_expiration
Definition: proposal_object.hpp:86
htlc_object.hpp
graphene::protocol::implementation_ids
@ implementation_ids
Definition: types.hpp:299
graphene::protocol::credit_deal_auto_repayment_type
credit_deal_auto_repayment_type
Defines automatic repayment types.
Definition: credit_offer.hpp:119
graphene::chain::asset_bitasset_data_object
contains properties that only apply to bitassets (market issued assets)
Definition: asset_object.hpp:255
graphene::chain::ticket_object::current_type
ticket_type current_type
The current type of the ticket.
Definition: ticket_object.hpp:69
graphene::chain::account_statistics_object::total_core_pob
share_type total_core_pob
Total amount of core token in active lock_forever tickets.
Definition: account_object.hpp:74
graphene::chain::asset_bitasset_data_object::current_feed
price_feed_with_icr current_feed
This is the currently active price feed, calculated from median_feed and other parameters.
Definition: asset_object.hpp:272
graphene::chain::asset_bitasset_data_object::median_feed
price_feed_with_icr median_feed
This is the median of values from the currently active feeds.
Definition: asset_object.hpp:270
fc::exception::to_detail_string
std::string to_detail_string(log_level ll=log_level::all) const
Definition: exception.cpp:183
graphene::chain::dynamic_global_property_object::total_pob
share_type total_pob
Definition: global_property_object.hpp:74
GRAPHENE_FEE_RATE_DENOM
constexpr uint32_t GRAPHENE_FEE_RATE_DENOM
Denominator for SameT Fund fee calculation.
Definition: config.hpp:121
graphene::protocol::generic_operation_result::updated_objects
flat_set< object_id_type > updated_objects
Definition: base.hpp:91
graphene::chain::database::head_block_num
uint32_t head_block_num() const
Definition: db_getter.cpp:72
graphene::chain::database::get_slot_at_time
uint32_t get_slot_at_time(fc::time_point_sec when) const
Definition: db_witness_schedule.cpp:74
graphene::protocol::share_type
safe< int64_t > share_type
Definition: types.hpp:309
graphene::chain::ticket_object::account
account_id_type account
The account who owns the ticket.
Definition: ticket_object.hpp:65
credit_offer_object.hpp
graphene::chain::account_statistics_object::total_pob_value
share_type total_pob_value
Total value of tickets whose current type is lock_forever.
Definition: account_object.hpp:80
graphene::chain::transaction_index
generic_index< transaction_history_object, transaction_multi_index_type > transaction_index
Definition: transaction_history_object.hpp:67
db_with.hpp
graphene::protocol::asset::amount
share_type amount
Definition: asset.hpp:36
graphene::db::undo_database::set_max_size
void set_max_size(size_t new_max_size)
Definition: undo_database.hpp:121
graphene::chain::asset_bitasset_data_object::get_black_swan_response_method
bitasset_options::black_swan_response_type get_black_swan_response_method() const
Get the effective black swan response method of this bitasset.
Definition: asset_object.hpp:349
graphene::chain::ticket_object::amount
asset amount
The token type and amount in the ticket.
Definition: ticket_object.hpp:67
graphene::protocol::price::base
asset base
Definition: asset.hpp:113
graphene::chain::database::get_account_stats_by_owner
const account_statistics_object & get_account_stats_by_owner(account_id_type owner) const
Definition: db_getter.cpp:142
GRAPHENE_IRREVERSIBLE_THRESHOLD
#define GRAPHENE_IRREVERSIBLE_THRESHOLD
Definition: config.hpp:45
graphene::chain::database::process_tickets
generic_operation_result process_tickets()
Definition: db_update.cpp:652
graphene::chain::database::get_global_properties
const global_property_object & get_global_properties() const
Definition: db_getter.cpp:47
fc::optional
provides stack-based nullable value similar to boost::optional
Definition: optional.hpp:20
graphene::chain::dynamic_global_property_object::maintenance_flag
@ maintenance_flag
Definition: global_property_object.hpp:117
market_object.hpp
graphene::chain::database::cancel_settle_order
void cancel_settle_order(const force_settlement_object &order)
Definition: db_market.cpp:524
withdraw_permission_object.hpp
GRAPHENE_ASSERT
#define GRAPHENE_ASSERT(expr, exc_type, FORMAT,...)
Definition: exceptions.hpp:28
witness_object.hpp
graphene::db::object_database::remove
void remove(const object &obj)
Definition: object_database.hpp:97
graphene::chain::database::get_dynamic_global_properties
const dynamic_global_property_object & get_dynamic_global_properties() const
Definition: db_getter.cpp:57
graphene::chain::database::skip_undo_history_check
@ skip_undo_history_check
used while reindexing
Definition: database.hpp:88
graphene::chain::database::find_least_collateralized_short
const call_order_object * find_least_collateralized_short(const asset_bitasset_data_object &bitasset, bool force_by_collateral_index) const
Definition: db_getter.cpp:161
global_property_object.hpp
graphene::chain::limit_order_object
an offer to sell an amount of an asset at a specified exchange rate by a certain time
Definition: market_object.hpp:45
graphene::protocol::lock_forever
@ lock_forever
Definition: ticket.hpp:39
graphene::protocol::ratio_type
boost::rational< int32_t > ratio_type
Definition: types.hpp:150
graphene::protocol::generic_operation_result
Definition: base.hpp:88
GRAPHENE_RECENTLY_MISSED_COUNT_DECREMENT
#define GRAPHENE_RECENTLY_MISSED_COUNT_DECREMENT
Definition: config.hpp:38
GRAPHENE_RECENTLY_MISSED_COUNT_INCREMENT
#define GRAPHENE_RECENTLY_MISSED_COUNT_INCREMENT
Definition: config.hpp:37
graphene::protocol::price::quote
asset quote
Definition: asset.hpp:114
GRAPHENE_100_PERCENT
#define GRAPHENE_100_PERCENT
Definition: config.hpp:102
graphene
Definition: api.cpp:48
ticket_object.hpp
graphene::protocol::generic_operation_result::removed_objects
flat_set< object_id_type > removed_objects
Definition: base.hpp:92
GRAPHENE_MAX_UNDO_HISTORY
#define GRAPHENE_MAX_UNDO_HISTORY
Definition: config.hpp:31
graphene::db::object_database::modify
void modify(const T &obj, const Lambda &m)
Definition: object_database.hpp:99
graphene::db::object_database::_undo_db
undo_database _undo_db
Definition: object_database.hpp:170
elog
#define elog(FORMAT,...)
Definition: logger.hpp:129
graphene::chain::account_statistics_object::total_core_pol
share_type total_core_pol
Total amount of core token in other tickets.
Definition: account_object.hpp:77
fc::safe
Definition: safe.hpp:26