Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement credit deal auto-repayment feature #2735

Merged
merged 29 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bba9b4a
Implement credit deal auto-repayment
abitmore Mar 19, 2023
cb4bc4a
Add tests for credit deal auto-repayment
abitmore Mar 22, 2023
5704816
Optimize processing of virutal operations
abitmore Mar 22, 2023
68efc09
Fix _current_virtual_op in push_proposal
abitmore Mar 22, 2023
e8fac66
Add tests for credit deal auto-repayment history
abitmore Mar 22, 2023
8a64036
Update params in credit deal auto-repayment tests
abitmore Mar 22, 2023
52d676a
Try to fix MinGW build with -Wa,-mbig-obj
abitmore Mar 22, 2023
bf5934f
Set VERBOSE=1 when making with MinGW in CI
abitmore Mar 22, 2023
d069614
Rename fee_parameters_type to fee_params_t
abitmore Mar 23, 2023
b7a5d68
Update a comment
abitmore Mar 23, 2023
2ca1d99
Update a comment
abitmore Mar 23, 2023
a89307c
Wrap long lines
abitmore Mar 24, 2023
9c0ad9b
Remove SonarCloud cache and threads configurtion as it is now by default
mpaladin Mar 24, 2023
43ea51e
Merge pull request #2736 from mpaladin/sonarcloud-configuration
abitmore Mar 24, 2023
5d59c9e
Add comments
abitmore Mar 24, 2023
70eece1
Merge pull request #2737 from bitshares/sonarcloud-configuration
abitmore Mar 25, 2023
b46de5d
Use gcovr to process code coverage data
abitmore Mar 29, 2023
11b2a81
Bump FC
abitmore Mar 29, 2023
f927efc
Fix macOS build
abitmore Mar 30, 2023
162584a
Merge pull request #2739 from bitshares/sonar-code-coverage
abitmore Mar 30, 2023
beb5022
Merge branch 'develop'
abitmore Mar 30, 2023
4f66004
Update sonar.projectVersion to 6.2.x
abitmore Mar 30, 2023
e68c738
Merge pull request #2740 from bitshares/update-sonar-project-version-…
abitmore Mar 30, 2023
d1da353
Remove mostly useless try/catch wrappers
abitmore Mar 31, 2023
5366a79
Call gcovr with --exclude-throw-branches in CI
abitmore Mar 31, 2023
3da7891
Exclude programs and tests from coverage reports
abitmore Mar 31, 2023
3f5c6c4
Merge pull request #2742 from bitshares/update-sonar
abitmore Apr 1, 2023
38ff734
Merge 'develop' branch
abitmore Apr 1, 2023
3fc4de4
Add fee_change_test
abitmore Apr 19, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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