Skip to content

Commit

Permalink
Merge pull request #2749 from bitshares/pr-2535-oso
Browse files Browse the repository at this point in the history
Implement Order-Sends-Take-Profit-Order feature
  • Loading branch information
abitmore committed Jun 19, 2023
2 parents 38dbc61 + ec47739 commit 9a36b7c
Show file tree
Hide file tree
Showing 18 changed files with 2,221 additions and 118 deletions.
25 changes: 16 additions & 9 deletions libraries/chain/db_block.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -310,19 +310,23 @@ processed_transaction database::validate_transaction( const signed_transaction&
return _apply_transaction( trx );
}

class push_proposal_nesting_guard {
class undo_session_nesting_guard {
public:
push_proposal_nesting_guard( uint32_t& nesting_counter, const database& db )
undo_session_nesting_guard( uint32_t& nesting_counter, const database& db )
: orig_value(nesting_counter), counter(nesting_counter)
{
FC_ASSERT( counter < db.get_global_properties().active_witnesses.size() * 2,
"Max proposal nesting depth exceeded!" );
counter++;
"Max undo session nesting depth exceeded!" );
++counter;
}
~push_proposal_nesting_guard()
~undo_session_nesting_guard()
{
if( --counter != orig_value )
elog( "Unexpected proposal nesting count value: ${n} != ${o}", ("n",counter)("o",orig_value) );
--counter;
// GCOVR_EXCL_START
// Defensive code, should not happen
if( counter != orig_value )
elog( "Unexpected undo session nesting count value: ${n} != ${o}", ("n",counter)("o",orig_value) );
// GCOVR_EXCL_STOP
}
private:
const uint32_t orig_value;
Expand All @@ -341,7 +345,7 @@ processed_transaction database::push_proposal(const proposal_object& proposal)
auto old_vop = _current_virtual_op;

try {
push_proposal_nesting_guard guard( _push_proposal_nesting_depth, *this );
undo_session_nesting_guard guard( _undo_session_nesting_depth, *this );
if( _undo_db.size() >= _undo_db.max_size() )
_undo_db.set_max_size( _undo_db.size() + 1 );
auto session = _undo_db.start_undo_session(true);
Expand Down Expand Up @@ -802,7 +806,10 @@ operation_result database::try_push_virtual_operation( transaction_evaluation_st

try
{
auto temp_session = _undo_db.start_undo_session();
undo_session_nesting_guard guard( _undo_session_nesting_depth, *this );
if( _undo_db.size() >= _undo_db.max_size() )
_undo_db.set_max_size( _undo_db.size() + 1 );
auto temp_session = _undo_db.start_undo_session(true);
auto result = apply_operation( eval_state, op ); // This is a virtual operation
temp_session.merge();
return result;
Expand Down
177 changes: 165 additions & 12 deletions libraries/chain/db_market.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,20 @@ void database::cancel_limit_order( const limit_order_object& order, bool create_
set_applied_operation_result( op_id, refunded );
}

cleanup_and_remove_limit_order( order );
}

void database::cleanup_and_remove_limit_order( const limit_order_object& order )
{
// Unlink the linked take profit order if it exists
if( order.take_profit_order_id.valid() )
{
const auto& take_profit_order = (*order.take_profit_order_id)(*this);
modify( take_profit_order, []( limit_order_object& loo ) {
loo.take_profit_order_id.reset();
});
}

remove(order);
}

Expand Down Expand Up @@ -1640,22 +1654,150 @@ asset database::match_impl( const force_settlement_object& settle,
} FC_CAPTURE_AND_RETHROW( (p_match_price)(max_settlement)(p_fill_price) // GCOVR_EXCL_LINE
(is_margin_call)(settle_is_taker) ) } // GCOVR_EXCL_LINE

optional<limit_order_id_type> database::process_limit_order_on_fill( const limit_order_object& order,
const asset& order_receives )
{
optional<limit_order_id_type> result;
if( order.on_fill.empty() )
return result;

const auto& take_profit_action = order.get_take_profit_action();

fc::uint128_t amount128( order_receives.amount.value );
amount128 *= take_profit_action.size_percent;
amount128 += (GRAPHENE_100_PERCENT - 1); // Round up
amount128 /= GRAPHENE_100_PERCENT;
// GCOVR_EXCL_START
// Defensive code, should not happen
if( amount128 <= 0 )
return result;
// GCOVR_EXCL_STOP

asset for_sale( static_cast<int64_t>( amount128 ), order_receives.asset_id );

if( order.take_profit_order_id.valid() ) // Update existing take profit order
{
limit_order_update_operation op;
op.seller = order.seller;
op.order = *order.take_profit_order_id;
op.delta_amount_to_sell = for_sale;

if( ( time_point_sec::maximum() - take_profit_action.expiration_seconds ) > head_block_time() )
op.new_expiration = head_block_time() + take_profit_action.expiration_seconds;
else
op.new_expiration = time_point_sec::maximum();

try
{
if( take_profit_action.fee_asset_id == asset_id_type() )
op.fee = current_fee_schedule().calculate_fee( op );
else
op.fee = current_fee_schedule().calculate_fee( op,
take_profit_action.fee_asset_id(*this).options.core_exchange_rate ); // This may throw

if( *order.take_profit_order_id > order.get_id() ) //The linked take profit order was generated by this order
{
// Update order price
const auto& take_profit_order = (*order.take_profit_order_id)(*this);
for_sale.amount += take_profit_order.for_sale;
auto sell_price = (~order.sell_price) * ratio_type( GRAPHENE_100_PERCENT,
int32_t(GRAPHENE_100_PERCENT) + take_profit_action.spread_percent );
auto new_min_to_receive = for_sale.multiply_and_round_up( sell_price ); // This may throw
op.new_price = for_sale / new_min_to_receive;
}
// else do not update order price

// GCOVR_EXCL_START
// Defensive code, should not fail
FC_ASSERT( !op.new_price || ( ~(*op.new_price) > order.sell_price ),
"Internal error: the take profit order should not match the current order" );
// GCOVR_EXCL_STOP

transaction_evaluation_state eval_state(this);
eval_state.skip_limit_order_price_check = true;

try_push_virtual_operation( eval_state, op );
}
catch( const fc::exception& e )
{
// We can in fact get here
// e.g. if the selling or receiving asset issuer blacklisted the account,
// or no sufficient balance to pay fees, or undo sessions nested too deeply
wlog( "At block ${n}, failed to process on_fill for limit order ${order}, "
"automatic action (maybe incomplete) was ${op}, exception was ${e}",
("op", operation(op))("order", order)
("n", head_block_num())("e", e.to_detail_string()) );
}
}
else // Create a new take profit order
{
limit_order_create_operation op;
op.seller = order.seller;
op.amount_to_sell = for_sale;
if( ( time_point_sec::maximum() - take_profit_action.expiration_seconds ) > head_block_time() )
op.expiration = head_block_time() + take_profit_action.expiration_seconds;
else
op.expiration = time_point_sec::maximum();
if( take_profit_action.repeat )
op.extensions.value.on_fill = order.on_fill;

try
{
if( take_profit_action.fee_asset_id == asset_id_type() )
op.fee = current_fee_schedule().calculate_fee( op );
else
op.fee = current_fee_schedule().calculate_fee( op,
take_profit_action.fee_asset_id(*this).options.core_exchange_rate ); // This may throw

auto sell_price = (~order.sell_price) * ratio_type( GRAPHENE_100_PERCENT,
int32_t(GRAPHENE_100_PERCENT) + take_profit_action.spread_percent );
op.min_to_receive = for_sale.multiply_and_round_up( sell_price ); // This may throw

// GCOVR_EXCL_START
// Defensive code, should not fail
FC_ASSERT( ~op.get_price() > order.sell_price,
"Internal error: the take profit order should not match the current order" );
// GCOVR_EXCL_STOP

transaction_evaluation_state eval_state(this);

auto op_result = try_push_virtual_operation( eval_state, op );
result = limit_order_id_type( op_result.get<object_id_type>() );
}
catch( const fc::exception& e )
{
// We can in fact get here
// e.g. if the selling or receiving asset issuer blacklisted the account,
// or no sufficient balance to pay fees, or undo sessions nested too deeply
wlog( "At block ${n}, failed to process on_fill for limit order ${order}, "
"automatic action (maybe incomplete) was ${op}, exception was ${e}",
("op", operation(op))("order", order)
("n", head_block_num())("e", e.to_detail_string()) );
}
}

return result;
}

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 {
if( head_block_time() < HARDFORK_555_TIME )
cull_if_small = true;

// GCOVR_EXCL_START
// Defensive code, normally none of these should fail
FC_ASSERT( order.amount_for_sale().asset_id == pays.asset_id );
FC_ASSERT( pays.asset_id != receives.asset_id );
// GCOVR_EXCL_STOP

const account_object& seller = order.seller(*this);

const auto issuer_fees = pay_market_fees(&seller, receives.asset_id(*this), receives, is_maker);

pay_order( seller, receives - issuer_fees, pays );
auto order_receives = receives - issuer_fees;
pay_order( seller, order_receives, pays );

assert( pays.asset_id != receives.asset_id );
push_applied_operation( fill_order_operation( order.id, order.seller, pays, receives,
issuer_fees, fill_price, is_maker ) );

Expand Down Expand Up @@ -1728,22 +1870,33 @@ bool database::fill_limit_order( const limit_order_object& order, const asset& p
}
}

// Process on_fill for order_receives
optional<limit_order_id_type> new_take_profit_order_id = process_limit_order_on_fill( order, order_receives );

// If this order is fully filled
if( pays == order.amount_for_sale() )
{
remove( order );
cleanup_and_remove_limit_order( order );
return true;
}
else

// This order is partially filled
if( new_take_profit_order_id.valid() ) // A new take profit order is created, link this order to it
{
modify( order, [&pays]( 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 );
return false;
modify( (*new_take_profit_order_id)(*this), [&order]( limit_order_object& loo ) {
loo.take_profit_order_id = order.get_id();
});
}
modify( order, [&pays,&new_take_profit_order_id]( limit_order_object& b ) {
b.for_sale -= pays.amount;
b.deferred_fee = 0;
b.deferred_paid_fee.amount = 0;
if( new_take_profit_order_id.valid() ) // A new take profit order is created, link it to this order
b.take_profit_order_id = *new_take_profit_order_id;
});
if( cull_if_small )
return maybe_cull_small_order( *this, order );
return false;
} FC_CAPTURE_AND_RETHROW( (pays)(receives) ) } // GCOVR_EXCL_LINE

/***
Expand Down
29 changes: 12 additions & 17 deletions libraries/chain/evaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,28 +79,23 @@ database& generic_evaluator::db()const { return trx_state->db(); }

void generic_evaluator::convert_fee()
{
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.accumulated_fees += fee_from_account.amount;
d.fee_pool -= core_fee_paid;
});
}
if( fee_asset->get_id() != asset_id_type() )
{
db().modify(*fee_asset_dyn_data, [this](asset_dynamic_data_object& d) {
d.accumulated_fees += fee_from_account.amount;
d.fee_pool -= core_fee_paid;
});
}
}

void generic_evaluator::pay_fee()
{ try {
if( !trx_state->skip_fee ) {
database& d = db();
/// TODO: db().pay_fee( account_id, core_fee );
d.modify(*fee_paying_account_statistics, [&](account_statistics_object& s)
{
s.pay_fee( core_fee_paid, d.get_global_properties().parameters.cashback_vesting_threshold );
});
}
} FC_CAPTURE_AND_RETHROW() }
database& d = db();
d.modify(*fee_paying_account_statistics, [this,&d](account_statistics_object& s)
{
s.pay_fee( core_fee_paid, d.get_global_properties().parameters.cashback_vesting_threshold );
});
} FC_CAPTURE_AND_RETHROW() } // GCOVR_EXCL_LINE

void generic_evaluator::pay_fba_fee( uint64_t fba_id )
{
Expand Down
6 changes: 6 additions & 0 deletions libraries/chain/hardfork.d/CORE_2535.hf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// bitshares-core issue #2535 Simple Order-Sends-Order (OSO)
#ifndef HARDFORK_CORE_2535_TIME
// Jan 1 2030, midnight; this is a dummy date until a hardfork date is scheduled
#define HARDFORK_CORE_2535_TIME (fc::time_point_sec( 1893456000 ))
#define HARDFORK_CORE_2535_PASSED(head_block_time) (head_block_time >= HARDFORK_CORE_2535_TIME)
#endif
2 changes: 1 addition & 1 deletion libraries/chain/include/graphene/chain/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

#define GRAPHENE_MAX_NESTED_OBJECTS (200)

const std::string GRAPHENE_CURRENT_DB_VERSION = "20230527";
const std::string GRAPHENE_CURRENT_DB_VERSION = "20230529";

#define GRAPHENE_RECENTLY_MISSED_COUNT_INCREMENT 4
#define GRAPHENE_RECENTLY_MISSED_COUNT_DECREMENT 3
Expand Down
10 changes: 8 additions & 2 deletions libraries/chain/include/graphene/chain/database.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,12 @@ namespace graphene { namespace chain {
share_type collateral_from_fund, const price_feed& current_feed );

private:
/// Clean up for a limit order and then remove it from database
void cleanup_and_remove_limit_order( const limit_order_object& order );
/// Process on_fill for a limit order
/// @return the ID of the newly created take profit order (in that case), otherwise null
optional<limit_order_id_type> process_limit_order_on_fill( const limit_order_object& order,
const asset& order_receives );
void _cancel_bids_and_revive_mpa( const asset_object& bitasset, const asset_bitasset_data_object& bad );
bool check_for_blackswan( const asset_object& mia, bool enable_black_swan = true,
const asset_bitasset_data_object* bitasset_ptr = nullptr );
Expand Down Expand Up @@ -820,8 +826,8 @@ namespace graphene { namespace chain {
*/
bool _opened = false;

// Counts nested proposal updates
uint32_t _push_proposal_nesting_depth = 0;
/// Counts nested undo sessions due to (for example) proposal updates or order-sends-order executions
uint32_t _undo_session_nesting_depth = 0;

/// Tracks assets affected by bitshares-core issue #453 before hard fork #615 in one block
flat_set<asset_id_type> _issue_453_affected_assets;
Expand Down
2 changes: 2 additions & 0 deletions libraries/chain/include/graphene/chain/market_evaluator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ namespace graphene { namespace chain {

private:
void process_deferred_fee();
/// Check if the linked take profit order is still compatible with the current order after update
bool is_linked_tp_order_compatible( const limit_order_update_operation& o ) const;

share_type _deferred_fee;
asset _deferred_paid_fee;
Expand Down
14 changes: 14 additions & 0 deletions libraries/chain/include/graphene/chain/market_object.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include <graphene/chain/types.hpp>
#include <graphene/db/generic_index.hpp>
#include <graphene/protocol/asset.hpp>
#include <graphene/protocol/market.hpp>

#include <boost/multi_index/composite_key.hpp>

Expand All @@ -52,6 +53,19 @@ class limit_order_object : public abstract_object<limit_order_object, protocol_i
asset deferred_paid_fee; ///< originally paid fee
bool is_settled_debt = false; ///< Whether this order is an individual settlement fund

/// Automatic actions when the limit order is filled or partially filled
vector< limit_order_auto_action > on_fill;

/// ID of the take profit limit order linked to this limit order
optional<limit_order_id_type> take_profit_order_id;

/// Returns the configured automatic action that will create a take profit order when this limit order is filled
const create_take_profit_order_action& get_take_profit_action() const
{
FC_ASSERT( !on_fill.empty() ); // Normally it should not fail // GCOVR_EXCL_LINE
return on_fill.front().get<create_take_profit_order_action>();
}

pair<asset_id_type,asset_id_type> get_market()const
{
auto tmp = std::make_pair( sell_price.base.asset_id, sell_price.quote.asset_id );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ namespace chain {
const signed_transaction* _trx = nullptr;
database* _db = nullptr;
bool _is_proposed_trx = false;
bool skip_fee = false;
bool skip_fee_schedule_check = false;
bool skip_limit_order_price_check = false; // Used in limit_order_update_op
};
} } // namespace graphene::chain
Loading

0 comments on commit 9a36b7c

Please sign in to comment.