diff --git a/libraries/chain/account_evaluator.cpp b/libraries/chain/account_evaluator.cpp index 0e9aed33..c8ef51f8 100644 --- a/libraries/chain/account_evaluator.cpp +++ b/libraries/chain/account_evaluator.cpp @@ -224,6 +224,7 @@ void_result account_upgrade_evaluator::do_apply(const account_upgrade_evaluator: if( o.upgrade_to_lifetime_member ) { // Upgrade to lifetime member. I don't care what the account was before. + a.statistics(d).process_fees(a, d); a.membership_expiration_date = time_point_sec::maximum(); a.referrer = a.registrar = a.lifetime_referrer = a.get_id(); a.lifetime_referrer_fee_percentage = GRAPHENE_100_PERCENT - a.network_fee_percentage; @@ -234,6 +235,7 @@ void_result account_upgrade_evaluator::do_apply(const account_upgrade_evaluator: a.membership_expiration_date += fc::days(365); } else { // Upgrade from basic account. + a.statistics(d).process_fees(a, d); assert(a.is_basic_account(d.head_block_time())); a.membership_expiration_date = d.head_block_time() + fc::days(365); } diff --git a/libraries/chain/account_object.cpp b/libraries/chain/account_object.cpp index 59ba30aa..512974e6 100644 --- a/libraries/chain/account_object.cpp +++ b/libraries/chain/account_object.cpp @@ -17,10 +17,24 @@ */ #include #include +#include #include namespace graphene { namespace chain { +share_type cut_fee(share_type a, uint16_t p) +{ + if( a == 0 || p == 0 ) + return 0; + if( p == GRAPHENE_100_PERCENT ) + return a; + + fc::uint128 r(a.value); + r *= p; + r /= GRAPHENE_100_PERCENT; + return r.to_uint64(); +} + bool account_object::is_authorized_asset(const asset_object& asset_obj) const { for( const auto id : blacklisting_accounts ) if( asset_obj.options.blacklist_authorities.find(id) != asset_obj.options.blacklist_authorities.end() ) return false; @@ -56,4 +70,65 @@ uint16_t account_statistics_object::calculate_bulk_discount_percent(const chain_ return bulk_discount_percent; } +void account_statistics_object::process_fees(const account_object& a, database& d) const +{ + if( pending_fees > 0 || pending_vested_fees > 0 ) + { + const auto& props = d.get_global_properties(); + + auto pay_out_fees = [&](const account_object& account, share_type core_fee_total, bool require_vesting) + { + share_type network_cut = cut_fee(core_fee_total, account.network_fee_percentage); + assert( network_cut <= core_fee_total ); + share_type burned = cut_fee(network_cut, props.parameters.burn_percent_of_fee); + share_type accumulated = network_cut - burned; + assert( accumulated + burned == network_cut ); + share_type lifetime_cut = cut_fee(core_fee_total, account.lifetime_referrer_fee_percentage); + share_type referral = core_fee_total - network_cut - lifetime_cut; + + d.modify(dynamic_asset_data_id_type()(d), [network_cut](asset_dynamic_data_object& d) { + d.accumulated_fees += network_cut; + }); + + // Potential optimization: Skip some of this math and object lookups by special casing on the account type. + // For example, if the account is a lifetime member, we can skip all this and just deposit the referral to + // it directly. + share_type referrer_cut = cut_fee(referral, account.referrer_rewards_percentage); + share_type registrar_cut = referral - referrer_cut; + + d.deposit_cashback(d.get(account.lifetime_referrer), lifetime_cut, require_vesting); + d.deposit_cashback(d.get(account.referrer), referrer_cut, require_vesting); + d.deposit_cashback(d.get(account.registrar), registrar_cut, require_vesting); + + assert( referrer_cut + registrar_cut + accumulated + burned + lifetime_cut == core_fee_total ); + }; + + share_type vesting_fee_subtotal(pending_fees); + share_type vested_fee_subtotal(pending_vested_fees); + share_type vesting_cashback, vested_cashback; + + if( lifetime_fees_paid > props.parameters.bulk_discount_threshold_min && + a.is_member(d.head_block_time()) ) + { + auto bulk_discount_rate = calculate_bulk_discount_percent(props.parameters); + vesting_cashback = cut_fee(vesting_fee_subtotal, bulk_discount_rate); + vesting_fee_subtotal -= vesting_cashback; + + vested_cashback = cut_fee(vested_fee_subtotal, bulk_discount_rate); + vested_fee_subtotal -= vested_cashback; + } + + pay_out_fees(a, vesting_fee_subtotal, true); + d.deposit_cashback(a, vesting_cashback, true); + pay_out_fees(a, vested_fee_subtotal, false); + d.deposit_cashback(a, vested_cashback, false); + + d.modify(*this, [vested_fee_subtotal, vesting_fee_subtotal](account_statistics_object& s) { + s.lifetime_fees_paid += vested_fee_subtotal + vesting_fee_subtotal; + s.pending_fees = 0; + s.pending_vested_fees = 0; + }); + } +} + } } // graphene::chain diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index 0db0b6f6..23f16537 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -357,77 +357,8 @@ void database::perform_chain_maintenance(const signed_block& next_block, const g process_fees_helper(database& d, const global_property_object& gpo) : d(d), props(gpo) {} - share_type cut_fee(share_type a, uint16_t p)const - { - if( a == 0 || p == 0 ) - return 0; - if( p == GRAPHENE_100_PERCENT ) - return a; - - fc::uint128 r(a.value); - r *= p; - r /= GRAPHENE_100_PERCENT; - return r.to_uint64(); - } - - void pay_out_fees(const account_object& account, share_type core_fee_total, bool require_vesting) - { - share_type network_cut = cut_fee(core_fee_total, account.network_fee_percentage); - assert( network_cut <= core_fee_total ); - share_type burned = cut_fee(network_cut, props.parameters.burn_percent_of_fee); - share_type accumulated = network_cut - burned; - assert( accumulated + burned == network_cut ); - share_type lifetime_cut = cut_fee(core_fee_total, account.lifetime_referrer_fee_percentage); - share_type referral = core_fee_total - network_cut - lifetime_cut; - - d.modify(dynamic_asset_data_id_type()(d), [network_cut](asset_dynamic_data_object& d) { - d.accumulated_fees += network_cut; - }); - - // Potential optimization: Skip some of this math and object lookups by special casing on the account type. - // For example, if the account is a lifetime member, we can skip all this and just deposit the referral to - // it directly. - share_type referrer_cut = cut_fee(referral, account.referrer_rewards_percentage); - share_type registrar_cut = referral - referrer_cut; - - d.deposit_cashback(d.get(account.lifetime_referrer), lifetime_cut, require_vesting); - d.deposit_cashback(d.get(account.referrer), referrer_cut, require_vesting); - d.deposit_cashback(d.get(account.registrar), registrar_cut, require_vesting); - - assert( referrer_cut + registrar_cut + accumulated + burned + lifetime_cut == core_fee_total ); - } - void operator()(const account_object& a) { - const account_statistics_object& stats = a.statistics(d); - - if( stats.pending_fees > 0 ) - { - share_type vesting_fee_subtotal(stats.pending_fees); - share_type vested_fee_subtotal(stats.pending_vested_fees); - share_type vesting_cashback, vested_cashback; - - if( stats.lifetime_fees_paid > props.parameters.bulk_discount_threshold_min && - a.is_member(d.head_block_time()) ) - { - auto bulk_discount_rate = stats.calculate_bulk_discount_percent(props.parameters); - vesting_cashback = cut_fee(vesting_fee_subtotal, bulk_discount_rate); - vesting_fee_subtotal -= vesting_cashback; - - vested_cashback = cut_fee(vested_fee_subtotal, bulk_discount_rate); - vested_fee_subtotal -= vested_cashback; - } - - pay_out_fees(a, vesting_fee_subtotal, true); - d.deposit_cashback(a, vesting_cashback, true); - pay_out_fees(a, vested_fee_subtotal, false); - d.deposit_cashback(a, vested_cashback, false); - - d.modify(stats, [vested_fee_subtotal, vesting_fee_subtotal](account_statistics_object& s) { - s.lifetime_fees_paid += vested_fee_subtotal + vesting_fee_subtotal; - s.pending_fees = 0; - s.pending_vested_fees = 0; - }); - } + a.statistics(d).process_fees(a, d); } } fee_helper(*this, gpo); diff --git a/libraries/chain/include/graphene/chain/account_object.hpp b/libraries/chain/include/graphene/chain/account_object.hpp index c41a15fc..0a3497d6 100644 --- a/libraries/chain/include/graphene/chain/account_object.hpp +++ b/libraries/chain/include/graphene/chain/account_object.hpp @@ -22,6 +22,7 @@ #include namespace graphene { namespace chain { +class database; /** * @class account_statistics_object @@ -64,8 +65,8 @@ namespace graphene { namespace chain { * them yet (registrar, referrer, lifetime referrer, network, etc). This is used as an optimization to avoid * doing massive amounts of uint128 arithmetic on each and every operation. * - *These fees will be paid out as vesting cash-back, and this counter will reset during the maintenance - *interval. + * These fees will be paid out as vesting cash-back, and this counter will reset during the maintenance + * interval. */ share_type pending_fees; /** @@ -76,6 +77,8 @@ namespace graphene { namespace chain { /// @brief Calculate the percentage discount this user receives on his fees uint16_t calculate_bulk_discount_percent(const chain_parameters& params)const; + /// @brief Split up and pay out @ref pending_fees and @ref pending_vested_fees + void process_fees(const account_object& a, database& d) const; }; /** @@ -189,6 +192,12 @@ namespace graphene { namespace chain { * Vesting balance which receives cashback_reward deposits. */ optional cashback_vb; + template + const vesting_balance_object& cashback_balance(const DB& db)const + { + FC_ASSERT(cashback_vb); + return db.get(*cashback_vb); + } /// @return true if this is a lifetime member account; false otherwise. bool is_lifetime_member()const diff --git a/libraries/chain/include/graphene/chain/vesting_balance_object.hpp b/libraries/chain/include/graphene/chain/vesting_balance_object.hpp index c0e32347..3d6ff6fb 100644 --- a/libraries/chain/include/graphene/chain/vesting_balance_object.hpp +++ b/libraries/chain/include/graphene/chain/vesting_balance_object.hpp @@ -98,8 +98,7 @@ namespace graphene { namespace chain { > vesting_policy; /** - * Timelocked balance object is a balance that is locked by the - * blockchain for a period of time. + * Vesting balance object is a balance that is locked by the blockchain for a period of time. */ class vesting_balance_object : public abstract_object { diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index 27d57445..61a5c8cf 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -270,11 +270,17 @@ void database_fixture::generate_blocks( uint32_t block_count ) generate_block(); } -void database_fixture::generate_blocks( fc::time_point_sec timestamp ) +void database_fixture::generate_blocks(fc::time_point_sec timestamp, bool miss_intermediate_blocks) { + if( miss_intermediate_blocks ) + { + auto slots_to_miss = db.get_slot_at_time(timestamp) - 1; + assert(slots_to_miss > 0); + generate_block(~0, generate_private_key("genesis"), slots_to_miss); + return; + } while( db.head_block_time() < timestamp ) generate_block(); - return; } account_create_operation database_fixture::make_account( @@ -683,14 +689,33 @@ void database_fixture::upgrade_to_lifetime_member( const account_object& account account_upgrade_operation op; op.account_to_upgrade = account.get_id(); op.upgrade_to_lifetime_member = true; + op.fee = op.calculate_fee(db.get_global_properties().parameters.current_fees); trx.operations = {op}; - db.push_transaction( trx, ~0 ); + db.push_transaction(trx, ~0); FC_ASSERT( op.account_to_upgrade(db).is_lifetime_member() ); trx.clear(); } FC_CAPTURE_AND_RETHROW((account)) } +void database_fixture::upgrade_to_annual_member(account_id_type account) +{ + upgrade_to_annual_member(account(db)); +} + +void database_fixture::upgrade_to_annual_member(const account_object& account) +{ + try { + account_upgrade_operation op; + op.account_to_upgrade = account.get_id(); + op.fee = op.calculate_fee(db.get_global_properties().parameters.current_fees); + trx.operations = {op}; + db.push_transaction(trx, ~0); + FC_ASSERT( op.account_to_upgrade(db).is_member(db.head_block_time()) ); + trx.clear(); + } FC_CAPTURE_AND_RETHROW((account)) +} + void database_fixture::print_market( const string& syma, const string& symb )const { const auto& limit_idx = db.get_index_type(); diff --git a/tests/common/database_fixture.hpp b/tests/common/database_fixture.hpp index a841aaf1..7010a29f 100644 --- a/tests/common/database_fixture.hpp +++ b/tests/common/database_fixture.hpp @@ -119,13 +119,13 @@ struct database_fixture { * @brief Generates block_count blocks * @param block_count number of blocks to generate */ - void generate_blocks( uint32_t block_count ); + void generate_blocks(uint32_t block_count); /** * @brief Generates blocks until the head block time matches or exceeds timestamp * @param timestamp target time to generate blocks until */ - void generate_blocks( fc::time_point_sec timestamp ); + void generate_blocks(fc::time_point_sec timestamp, bool miss_intermediate_blocks = false); account_create_operation make_account( const std::string& name = "nathan", @@ -206,6 +206,8 @@ struct database_fixture { void enable_fees( share_type fee = GRAPHENE_BLOCKCHAIN_PRECISION ); void upgrade_to_lifetime_member( account_id_type account ); void upgrade_to_lifetime_member( const account_object& account ); + void upgrade_to_annual_member( account_id_type account ); + void upgrade_to_annual_member( const account_object& account ); void print_market( const string& syma, const string& symb )const; string pretty( const asset& a )const; void print_short_order( const short_order_object& cur )const; diff --git a/tests/tests/fee_tests.cpp b/tests/tests/fee_tests.cpp index 03816ecb..e72ca4d0 100644 --- a/tests/tests/fee_tests.cpp +++ b/tests/tests/fee_tests.cpp @@ -16,6 +16,8 @@ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +#include + #include #include "../common/database_fixture.hpp" @@ -36,15 +38,15 @@ BOOST_AUTO_TEST_CASE( cashback_test ) * /----------------\ /----------------\ | * * | ann (Annual) | | dumy (basic) | | * * \----------------/ \----------------/ |-------------. * - * | Refers & L--------------------------------. | | * - * v Registers Refers v v | * + * | Refers L--------------------------------. | | * + * v Refers v v | * * /----------------\ /----------------\ | * - * | scud (basic) | | stud (basic) | | * - * \----------------/ | (Upgrades to | | * + * | scud (basic) |<------------------------| stud (basic) | | * + * \----------------/ Registers | (Upgrades to | | * * | Lifetime) | v * * \----------------/ /--------------\ * * L------->| pleb (Basic) | * - * \--------------/ * + * Refers \--------------/ * * * */ ACTOR(life); @@ -62,6 +64,7 @@ BOOST_AUTO_TEST_CASE( cashback_test ) const auto& fees = db.get_global_properties().parameters.current_fees; #define CustomRegisterActor(actor_name, registrar_name, referrer_name, referrer_rate) \ + account_id_type actor_name ## _id; \ { \ account_create_operation op; \ op.registrar = registrar_name ## _id; \ @@ -74,10 +77,31 @@ BOOST_AUTO_TEST_CASE( cashback_test ) op.fee = op.calculate_fee(fees); \ trx.operations = {op}; \ trx.sign(registrar_name ## _key_id, registrar_name ## _private_key); \ - db.push_transaction(trx); \ + actor_name ## _id = db.push_transaction(trx).operation_results.front().get(); \ + trx.clear(); \ } CustomRegisterActor(ann, life, life, 75); + + transfer(life_id, ann_id, asset(1000000)); + upgrade_to_annual_member(ann_id); + + CustomRegisterActor(dumy, reggie, life, 75); + CustomRegisterActor(stud, reggie, ann, 80); + + transfer(life_id, stud_id, asset(1000000)); + upgrade_to_lifetime_member(stud_id); + + CustomRegisterActor(pleb, reggie, stud, 95); + CustomRegisterActor(scud, stud, ann, 80); + + generate_block(); + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time, true); + + BOOST_CHECK_EQUAL(life_id(db).cashback_balance(db).balance.amount.value, 78000); + BOOST_CHECK_EQUAL(reggie_id(db).cashback_balance(db).balance.amount.value, 34000); + BOOST_CHECK_EQUAL(ann_id(db).cashback_balance(db).balance.amount.value, 40000); + BOOST_CHECK_EQUAL(stud_id(db).cashback_balance(db).balance.amount.value, 8000); } FC_LOG_AND_RETHROW() } BOOST_AUTO_TEST_SUITE_END()