Skip to content

Commit

Permalink
Merge pull request #2735 from bitshares/pr-2595-credit-auto-repay
Browse files Browse the repository at this point in the history
Implement credit deal auto-repayment feature
  • Loading branch information
abitmore authored Apr 19, 2023
2 parents 1496be5 + 3fc4de4 commit d441b3e
Show file tree
Hide file tree
Showing 72 changed files with 1,303 additions and 489 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.win.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,4 @@ jobs:
run: |
export CCACHE_DIR="$GITHUB_WORKSPACE/ccache"
mkdir -p "$CCACHE_DIR"
make -j 2 -C _build witness_node cli_wallet
make VERBOSE=1 -j 2 -C _build witness_node cli_wallet
26 changes: 8 additions & 18 deletions .github/workflows/sonar-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ jobs:
sudo apt-get install -y --allow-downgrades openssl=${openssl_ver} libssl-dev=${libssl_ver}
sudo apt-get install -y \
ccache \
gcovr \
parallel \
libboost-thread-dev \
libboost-iostreams-dev \
Expand Down Expand Up @@ -109,7 +110,6 @@ jobs:
with:
path: |
ccache
sonar_cache
key: sonar-${{ env.OS_VERSION }}-${{ github.ref }}-${{ github.sha }}
restore-keys: |
sonar-${{ env.OS_VERSION }}-${{ github.ref }}-
Expand Down Expand Up @@ -185,24 +185,14 @@ jobs:
df -h
- name: Prepare for scanning with SonarScanner
run: |
mkdir -p sonar_cache
find _build/libraries/[acdenptuw]*/CMakeFiles/*.dir \
_build/libraries/plugins/*/CMakeFiles/*.dir \
-type d -print \
| while read d; do \
tmpd="${d:7}"; \
srcd="${tmpd/CMakeFiles*.dir/.}"; \
gcov -o "$d" "${srcd}"/*.[ch][px][px] \
"${srcd}"/include/graphene/*/*.[ch][px][px] ; \
done >/dev/null
find _build/programs/[cdgjsw]*/CMakeFiles/*.dir \
-type d -print \
| while read d; do \
tmpd="${d:7}"; \
srcd="${tmpd/CMakeFiles*.dir/.}"; \
gcov -o "$d" "${srcd}"/*.[ch][px][px] ; \
done >/dev/null
programs/build_helpers/set_sonar_branch_for_github_actions sonar-project.properties
pushd _build
gcovr --version
gcovr --exclude-unreachable-branches --exclude-throw-branches \
--exclude '\.\./programs/' \
--exclude '\.\./tests/' \
--sonarqube ../coverage.xml -r ..
popd
- name: Scan with SonarScanner
env:
# to get access to secrets.SONAR_TOKEN, provide GITHUB_TOKEN
Expand Down
40 changes: 39 additions & 1 deletion libraries/chain/credit_offer_evaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,13 @@ void_result credit_offer_update_evaluator::do_apply( const credit_offer_update_o
void_result credit_offer_accept_evaluator::do_evaluate(const credit_offer_accept_operation& op)
{ try {
const database& d = db();
const auto block_time = d.head_block_time();

if( !HARDFORK_CORE_2595_PASSED(block_time) )
{
FC_ASSERT( !op.extensions.value.auto_repay.valid(),
"auto_repay unavailable until the core-2595 hardfork");
}

_offer = &op.offer_id(d);

Expand Down Expand Up @@ -325,6 +332,7 @@ extendable_operation_result credit_offer_accept_evaluator::do_apply( const credi
obj.collateral_amount = op.collateral.amount;
obj.fee_rate = _offer->fee_rate;
obj.latest_repay_time = repay_time;
obj.auto_repay = ( op.extensions.value.auto_repay.valid() ? *op.extensions.value.auto_repay : 0 );
});

if( _deal_summary != nullptr )
Expand Down Expand Up @@ -377,7 +385,7 @@ void_result credit_deal_repay_evaluator::do_evaluate(const credit_deal_repay_ope

// Note: the result can be larger than 64 bit, but since we don't store it, it is allowed
auto required_fee = ( ( ( fc::uint128_t( op.repay_amount.amount.value ) * _deal->fee_rate )
+ GRAPHENE_FEE_RATE_DENOM ) - 1 ) / GRAPHENE_FEE_RATE_DENOM; // Round up
+ GRAPHENE_FEE_RATE_DENOM ) - 1 ) / GRAPHENE_FEE_RATE_DENOM; // Round up

FC_ASSERT( fc::uint128_t(op.credit_fee.amount.value) >= required_fee,
"Insuffient credit fee, requires ${r}, offered ${p}",
Expand Down Expand Up @@ -442,6 +450,9 @@ extendable_operation_result credit_deal_repay_evaluator::do_apply( const credit_
}
else // to partially repay
{
// Note:
// Due to rounding, it is possible that the account is paying too much debt asset for too little collateral,
// in extreme cases, the amount to release can be zero.
auto amount_to_release = ( fc::uint128_t( op.repay_amount.amount.value ) * _deal->collateral_amount.value )
/ _deal->debt_amount.value; // Round down
FC_ASSERT( amount_to_release < fc::uint128_t( _deal->collateral_amount.value ), "Internal error" );
Expand All @@ -461,4 +472,31 @@ extendable_operation_result credit_deal_repay_evaluator::do_apply( const credit_
return result;
} FC_CAPTURE_AND_RETHROW( (op) ) }

void_result credit_deal_update_evaluator::do_evaluate(const credit_deal_update_operation& op)
{
const database& d = db();
const auto block_time = d.head_block_time();

FC_ASSERT( HARDFORK_CORE_2595_PASSED(block_time), "Not allowed until the core-2595 hardfork" );

_deal = &op.deal_id(d);

FC_ASSERT( _deal->borrower == op.account, "A credit deal can only be updated by the borrower" );

FC_ASSERT( _deal->auto_repay != op.auto_repay, "The automatic repayment type does not change" );

return void_result();
}

void_result credit_deal_update_evaluator::do_apply( const credit_deal_update_operation& op) const
{
database& d = db();

d.modify( *_deal, [&op]( credit_deal_object& obj ){
obj.auto_repay = op.auto_repay;
});

return void_result();
}

} } // graphene::chain
27 changes: 27 additions & 0 deletions libraries/chain/db_block.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ processed_transaction database::push_proposal(const proposal_object& proposal)
processed_transaction ptrx(proposal.proposed_transaction);
eval_state._trx = &ptrx;
size_t old_applied_ops_size = _applied_ops.size();
auto old_vop = _current_virtual_op;

try {
push_proposal_nesting_guard guard( _push_proposal_nesting_depth, *this );
Expand All @@ -360,6 +361,7 @@ processed_transaction database::push_proposal(const proposal_object& proposal)
}
else
{
_current_virtual_op = old_vop;
_applied_ops.resize( old_applied_ops_size );
}
wlog( "${e}", ("e",e.to_detail_string() ) );
Expand Down Expand Up @@ -783,6 +785,31 @@ operation_result database::apply_operation( transaction_evaluation_state& eval_s
return result;
} FC_CAPTURE_AND_RETHROW( (op) ) }

operation_result database::try_push_virtual_operation( transaction_evaluation_state& eval_state, const operation& op )
{
operation_validate( op );

// Note: these variables could be updated during the apply_operation() call
size_t old_applied_ops_size = _applied_ops.size();
auto old_vop = _current_virtual_op;

try
{
auto temp_session = _undo_db.start_undo_session();
auto result = apply_operation( eval_state, op ); // This is a virtual operation
temp_session.merge();
return result;
}
catch( const fc::exception& e )
{
wlog( "Failed to push virtual operation ${op} at block ${n}; exception was ${e}",
("op", op)("n", head_block_num())("e", e.to_detail_string()) );
_current_virtual_op = old_vop;
_applied_ops.resize( old_applied_ops_size );
throw;
}
}

const witness_object& database::validate_block_header( uint32_t skip, const signed_block& next_block )const
{
FC_ASSERT( head_block_id() == next_block.previous, "", ("head_block_id",head_block_id())("next.prev",next_block.previous) );
Expand Down
1 change: 1 addition & 0 deletions libraries/chain/db_init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ void database::initialize_evaluators()
register_evaluator<credit_offer_update_evaluator>();
register_evaluator<credit_offer_accept_evaluator>();
register_evaluator<credit_deal_repay_evaluator>();
register_evaluator<credit_deal_update_evaluator>();
}

void database::initialize_indexes()
Expand Down
4 changes: 4 additions & 0 deletions libraries/chain/db_notify.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,10 @@ struct get_impacted_account_visitor
_impacted.insert( op.offer_owner );
_impacted.insert( op.borrower );
}
void operator()( const credit_deal_update_operation& op )
{
_impacted.insert( op.fee_payer() ); // account
}
};

} // namespace detail
Expand Down
57 changes: 57 additions & 0 deletions libraries/chain/db_update.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,63 @@ void database::update_credit_offers_and_deals()
{
const credit_deal_object& deal = *deal_itr;

// Process automatic repayment
// Note: an automatic repayment may fail, in which case we consider the credit deal past due without repayment
using repay_type = credit_deal_auto_repayment_type;
if( static_cast<uint8_t>(repay_type::no_auto_repayment) != deal.auto_repay )
{
credit_deal_repay_operation op;
op.account = deal.borrower;
op.deal_id = deal.get_id();
// Amounts
// Note: the result can be larger than 64 bit
auto required_fee = ( ( ( fc::uint128_t( deal.debt_amount.value ) * deal.fee_rate )
+ GRAPHENE_FEE_RATE_DENOM ) - 1 ) / GRAPHENE_FEE_RATE_DENOM; // Round up
fc::uint128_t total_required = required_fee + deal.debt_amount.value;
auto balance = get_balance( deal.borrower, deal.debt_asset );
if( static_cast<uint8_t>(repay_type::only_full_repayment) == deal.auto_repay
|| fc::uint128_t( balance.amount.value ) >= total_required )
{ // if only full repayment or account balance is sufficient
op.repay_amount = asset( deal.debt_amount, deal.debt_asset );
op.credit_fee = asset( static_cast<int64_t>( required_fee ), deal.debt_asset );
}
else // Allow partial repayment
{
fc::uint128_t debt_to_repay = ( fc::uint128_t( balance.amount.value ) * GRAPHENE_FEE_RATE_DENOM )
/ ( GRAPHENE_FEE_RATE_DENOM + deal.fee_rate ); // Round down
fc::uint128_t collateral_to_release = ( debt_to_repay * deal.collateral_amount.value )
/ deal.debt_amount.value; // Round down
debt_to_repay = ( ( ( collateral_to_release * deal.debt_amount.value ) + deal.collateral_amount.value )
- 1 ) / deal.collateral_amount.value; // Round up
fc::uint128_t fee_to_pay = ( ( ( debt_to_repay * deal.fee_rate )
+ GRAPHENE_FEE_RATE_DENOM ) - 1 ) / GRAPHENE_FEE_RATE_DENOM; // Round up
op.repay_amount = asset( static_cast<int64_t>( debt_to_repay ), deal.debt_asset );
op.credit_fee = asset( static_cast<int64_t>( fee_to_pay ), deal.debt_asset );
}

auto deal_copy = deal; // Make a copy for logging

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

try
{
try_push_virtual_operation( eval_state, op );
}
catch( const fc::exception& e )
{
// We can in fact get here,
// e.g. if the debt asset issuer blacklisted the account, or account balance is insufficient
wlog( "Automatic repayment ${op} for credit deal ${credit_deal} failed at block ${n}; "
"account balance was ${balance}; exception was ${e}",
("op", op)("credit_deal", deal_copy)
("n", head_block_num())("balance", balance)("e", e.to_detail_string()) );
}

if( !find( op.deal_id ) ) // The credit deal is fully repaid
continue;
}

// Update offer
// Note: offer balance can be zero after updated. TODO remove zero-balance offers after a period
const credit_offer_object& offer = deal.offer_id(*this);
Expand Down
6 changes: 6 additions & 0 deletions libraries/chain/hardfork.d/CORE_2595.hf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// bitshares-core issue #2595 Credit deal auto-repayment
#ifndef HARDFORK_CORE_2595_TIME
// Jan 1 2030, midnight; this is a dummy date until a hardfork date is scheduled
#define HARDFORK_CORE_2595_TIME (fc::time_point_sec( 1893456000 ))
#define HARDFORK_CORE_2595_PASSED(head_block_time) (head_block_time >= HARDFORK_CORE_2595_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 = "20230320";
const std::string GRAPHENE_CURRENT_DB_VERSION = "20230322";

#define GRAPHENE_RECENTLY_MISSED_COUNT_INCREMENT 4
#define GRAPHENE_RECENTLY_MISSED_COUNT_DECREMENT 3
Expand Down
11 changes: 11 additions & 0 deletions libraries/chain/include/graphene/chain/credit_offer_evaluator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,15 @@ namespace graphene { namespace chain {
const credit_deal_object* _deal = nullptr;
};

class credit_deal_update_evaluator : public evaluator<credit_deal_update_evaluator>
{
public:
using operation_type = credit_deal_update_operation;

void_result do_evaluate( const credit_deal_update_operation& op );
void_result do_apply( const credit_deal_update_operation& op ) const;

const credit_deal_object* _deal = nullptr;
};

} } // graphene::chain
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class credit_deal_object : public abstract_object<credit_deal_object, protocol_i
share_type collateral_amount; ///< How much funds in collateral
uint32_t fee_rate = 0; ///< Fee rate, the demominator is GRAPHENE_FEE_RATE_DENOM
time_point_sec latest_repay_time; ///< The deadline when the debt should be repaid
uint8_t auto_repay; ///< The specified automatic repayment type
};

struct by_latest_repay_time; // for protocol
Expand Down
5 changes: 5 additions & 0 deletions libraries/chain/include/graphene/chain/database.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,11 @@ namespace graphene { namespace chain {
void _apply_block( const signed_block& next_block );
processed_transaction _apply_transaction( const signed_transaction& trx );

/// Validate, evaluate and apply a virtual operation using a temporary undo_database session,
/// if fail, rewind any changes made
operation_result try_push_virtual_operation( transaction_evaluation_state& eval_state,
const operation& op );

///Steps involved in applying a new block
///@{

Expand Down
5 changes: 5 additions & 0 deletions libraries/chain/include/graphene/chain/hardfork_visitor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ struct hardfork_visitor {
protocol::credit_offer_accept_operation,
protocol::credit_deal_repay_operation,
protocol::credit_deal_expired_operation >;
using credit_deal_update_op = fc::typelist::list< protocol::credit_deal_update_operation >;

fc::time_point_sec now;

/// @note using head block time for all operations
Expand All @@ -92,6 +94,9 @@ struct hardfork_visitor {
std::enable_if_t<fc::typelist::contains<credit_offer_ops, Op>(), bool>
visit() { return HARDFORK_CORE_2362_PASSED(now); }
template<typename Op>
std::enable_if_t<fc::typelist::contains<credit_deal_update_op, Op>(), bool>
visit() { return HARDFORK_CORE_2595_PASSED(now); }
template<typename Op>
std::enable_if_t<fc::typelist::contains<liquidity_pool_update_op, Op>(), bool>
visit() { return HARDFORK_CORE_2604_PASSED(now); }
/// @}
Expand Down
13 changes: 12 additions & 1 deletion libraries/chain/proposal_evaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@ struct proposal_operation_hardfork_visitor
FC_ASSERT(!op.new_parameters.current_fees->exists<credit_deal_expired_operation>(),
"Unable to define fees for credit offer operations prior to the core-2362 hardfork");
}
if (!HARDFORK_CORE_2595_PASSED(block_time)) {
FC_ASSERT(!op.new_parameters.current_fees->exists<credit_deal_update_operation>(),
"Unable to define fees for credit deal update operation prior to the core-2595 hardfork");
}
if (!HARDFORK_CORE_2604_PASSED(block_time)) {
FC_ASSERT(!op.new_parameters.current_fees->exists<liquidity_pool_update_operation>(),
"Unable to define fees for liquidity pool update operation prior to the core-2604 hardfork");
Expand Down Expand Up @@ -286,13 +290,20 @@ struct proposal_operation_hardfork_visitor
void operator()(const graphene::chain::credit_offer_update_operation&) const {
FC_ASSERT( HARDFORK_CORE_2362_PASSED(block_time), "Not allowed until the core-2362 hardfork" );
}
void operator()(const graphene::chain::credit_offer_accept_operation&) const {
void operator()(const graphene::chain::credit_offer_accept_operation& op) const {
FC_ASSERT( HARDFORK_CORE_2362_PASSED(block_time), "Not allowed until the core-2362 hardfork" );
if( !HARDFORK_CORE_2595_PASSED(block_time) ) {
FC_ASSERT( !op.extensions.value.auto_repay.valid(),
"auto_repay unavailable until the core-2595 hardfork");
}
}
void operator()(const graphene::chain::credit_deal_repay_operation&) const {
FC_ASSERT( HARDFORK_CORE_2362_PASSED(block_time), "Not allowed until the core-2362 hardfork" );
}
// Note: credit_deal_expired_operation is a virtual operation thus no need to add code here
void operator()(const graphene::chain::credit_deal_update_operation&) const {
FC_ASSERT( HARDFORK_CORE_2595_PASSED(block_time), "Not allowed until the core-2595 hardfork" );
}

// loop and self visit in proposals
void operator()(const graphene::chain::proposal_create_operation &v) const {
Expand Down
1 change: 1 addition & 0 deletions libraries/chain/small_objects.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ FC_REFLECT_DERIVED_NO_TYPENAME( graphene::chain::credit_deal_object, (graphene::
(collateral_amount)
(fee_rate)
(latest_repay_time)
(auto_repay)
)

FC_REFLECT_DERIVED_NO_TYPENAME( graphene::chain::credit_deal_summary_object, (graphene::db::object),
Expand Down
Loading

0 comments on commit d441b3e

Please sign in to comment.