From 9b08b502bed3855bc42725d6cfc934829d41f766 Mon Sep 17 00:00:00 2001 From: Roman Olearski Date: Tue, 4 Jul 2017 16:55:34 +0200 Subject: [PATCH 01/19] implementing betting_market_resolve_group_operation (3) #1 --- libraries/app/impacted.cpp | 4 +- libraries/chain/betting_market_evaluator.cpp | 10 +- libraries/chain/db_bet.cpp | 156 ++++++++++++------ libraries/chain/db_init.cpp | 2 +- libraries/chain/db_notify.cpp | 4 +- .../chain/betting_market_evaluator.hpp | 11 +- .../chain/include/graphene/chain/database.hpp | 6 +- .../chain/protocol/betting_market.hpp | 48 +++--- .../graphene/chain/protocol/operations.hpp | 4 +- libraries/chain/protocol/betting_market.cpp | 4 +- .../wallet/include/graphene/wallet/wallet.hpp | 8 +- libraries/wallet/wallet.cpp | 14 +- tests/betting/betting_tests.cpp | 21 ++- tests/common/database_fixture.cpp | 13 +- tests/common/database_fixture.hpp | 2 +- 15 files changed, 195 insertions(+), 112 deletions(-) diff --git a/libraries/app/impacted.cpp b/libraries/app/impacted.cpp index 3038c8f7..7a5370dc 100644 --- a/libraries/app/impacted.cpp +++ b/libraries/app/impacted.cpp @@ -209,7 +209,7 @@ struct get_impacted_account_visitor void operator()( const event_create_operation& op ) {} void operator()( const betting_market_group_create_operation& op ) {} void operator()( const betting_market_create_operation& op ) {} - void operator()( const betting_market_resolve_operation& op ) {} + void operator()( const betting_market_group_resolve_operation& op ) {} void operator()( const bet_place_operation& op ) { _impacted.insert( op.bettor_id ); @@ -226,7 +226,7 @@ struct get_impacted_account_visitor { _impacted.insert( op.bettor_id ); } - void operator()( const betting_market_resolved_operation& op ) + void operator()( const betting_market_group_resolved_operation& op ) { _impacted.insert( op.bettor_id ); } diff --git a/libraries/chain/betting_market_evaluator.cpp b/libraries/chain/betting_market_evaluator.cpp index 98173cfe..d55042e0 100644 --- a/libraries/chain/betting_market_evaluator.cpp +++ b/libraries/chain/betting_market_evaluator.cpp @@ -191,17 +191,19 @@ void_result bet_cancel_evaluator::do_apply(const bet_cancel_operation& op) return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } -void_result betting_market_resolve_evaluator::do_evaluate(const betting_market_resolve_operation& op) +void_result betting_market_group_resolve_evaluator::do_evaluate(const betting_market_group_resolve_operation& op) { try { const database& d = db(); - _betting_market = &op.betting_market_id(d); + _betting_market_group = &op.betting_market_group_id(d); + db().validate_betting_market_group_resolutions(*_betting_market_group, op.resolutions); return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } -void_result betting_market_resolve_evaluator::do_apply(const betting_market_resolve_operation& op) +void_result betting_market_group_resolve_evaluator::do_apply(const betting_market_group_resolve_operation& op) { try { - db().resolve_betting_market(*_betting_market, op.resolution); + db().resolve_betting_market_group(*_betting_market_group, op.resolutions); return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } + } } // graphene::chain diff --git a/libraries/chain/db_bet.cpp b/libraries/chain/db_bet.cpp index 2c5c9374..776640d9 100644 --- a/libraries/chain/db_bet.cpp +++ b/libraries/chain/db_bet.cpp @@ -44,64 +44,126 @@ void database::cancel_all_betting_markets_for_event(const event_object& event_ob for (const betting_market_group_object& betting_market_group : boost::make_iterator_range(betting_market_group_index.equal_range(event_obj.id))) { - //for each betting market in the betting market group - auto betting_market_itr = betting_market_index.lower_bound(betting_market_group.id); - while (betting_market_itr != betting_market_index.end() && - betting_market_itr->group_id == betting_market_group.id) - { - auto old_betting_market_itr = betting_market_itr; - ++betting_market_itr; - resolve_betting_market(*old_betting_market_itr, betting_market_resolution_type::cancel); - } + resolve_betting_market_group(betting_market_group, {}); } - //TODO: should we remove market groups once all their markets are resolved? } -void database::resolve_betting_market(const betting_market_object& betting_market, - betting_market_resolution_type resolution) +void database::validate_betting_market_group_resolutions(const betting_market_group_object& betting_market_group, + const std::map& resolutions) { - cancel_all_unmatched_bets_on_betting_market(betting_market); + auto& betting_market_index = get_index_type().indices().get(); + auto betting_market_itr = betting_market_index.lower_bound(betting_market_group.id); + while (betting_market_itr != betting_market_index.end() && betting_market_itr->group_id == betting_market_group.id) + { + const betting_market_object& betting_market = *betting_market_itr; + // every betting market in the group tied with resolution + assert(resolutions.count(betting_market.id)); + ++betting_market_itr; + } +} - auto& index = get_index_type().indices().get(); - auto position_itr = index.lower_bound(std::make_tuple(betting_market.id)); - while (position_itr != index.end() && - position_itr->betting_market_id == betting_market.id) - { - const betting_market_position_object& position = *position_itr; - ++position_itr; +void database::resolve_betting_market_group(const betting_market_group_object& betting_market_group, + const std::map& resolutions) +{ + bool cancel = resolutions.size() == 0; + // collecting bettors and their positions + std::map > bettor_positions_map; - share_type payout_amount = 0; - switch (resolution) + auto& betting_market_index = get_index_type().indices().get(); + auto betting_market_itr = betting_market_index.lower_bound(betting_market_group.id); + while (betting_market_itr != betting_market_index.end() && betting_market_itr->group_id == betting_market_group.id) + { + const betting_market_object& betting_market = *betting_market_itr; + ++betting_market_itr; + cancel_all_unmatched_bets_on_betting_market(betting_market); + + auto& index = get_index_type().indices().get(); + auto position_itr = index.lower_bound(std::make_tuple(betting_market.id)); + + while (position_itr != index.end() && position_itr->betting_market_id == betting_market.id) + { + const betting_market_position_object& position = *position_itr; + ++position_itr; + + bettor_positions_map[position.bettor_id].push_back(&position); + } + } + + // walking through bettors' positions and collecting winings and fees respecting asset_id + for (const auto& bettor_positions_pair: bettor_positions_map) + { + std::map payout_amounts; + std::map fees_collected; + account_id_type bettor_id = bettor_positions_pair.first; + const std::vector& bettor_positions = bettor_positions_pair.second; + + for (const betting_market_position_object* position : bettor_positions) { - case betting_market_resolution_type::win: - payout_amount += position.pay_if_payout_condition; - payout_amount += position.pay_if_not_canceled; - //TODO: pay the fees to the correct (dividend-distribution) account - adjust_balance(account_id_type(), asset(position.fees_collected, betting_market.asset_id)); - break; - case betting_market_resolution_type::not_win: - payout_amount += position.pay_if_not_payout_condition; - payout_amount += position.pay_if_not_canceled; - //TODO: pay the fees to the correct (dividend-distribution) account - adjust_balance(account_id_type(), asset(position.fees_collected, betting_market.asset_id)); - break; - case betting_market_resolution_type::cancel: - payout_amount += position.pay_if_canceled; - payout_amount += position.fees_collected; - break; + betting_market_resolution_type resolution; + if (cancel) + { + resolution = betting_market_resolution_type::cancel; + } + else + { + // checked in evaluator, should never happen, see above + assert(resolutions.count(position->betting_market_id)); + resolution = resolutions.at(position->betting_market_id); + } + const betting_market_object& betting_market = position->betting_market_id(*this); + + switch (resolution) + { + case betting_market_resolution_type::win: + payout_amounts[betting_market.asset_id] += position->pay_if_payout_condition; + payout_amounts[betting_market.asset_id] += position->pay_if_not_canceled; + fees_collected[betting_market.asset_id] += position->fees_collected; + break; + case betting_market_resolution_type::not_win: + payout_amounts[betting_market.asset_id] += position->pay_if_not_payout_condition; + payout_amounts[betting_market.asset_id] += position->pay_if_not_canceled; + fees_collected[betting_market.asset_id] += position->fees_collected; + break; + case betting_market_resolution_type::cancel: + payout_amounts[betting_market.asset_id] += position->pay_if_canceled; + payout_amounts[betting_market.asset_id] += position->fees_collected; + break; + default: + continue; + } + remove(*position); } - adjust_balance(position.bettor_id, asset(payout_amount, betting_market.asset_id)); + std::vector winnings; + std::vector fees; + for (const auto& payout_amount_pair: payout_amounts) + { + asset payout = asset(payout_amount_pair.second, payout_amount_pair.first); + adjust_balance(bettor_id, payout); + winnings.push_back(payout); + } + for (const auto& fee_collected_pair: fees_collected) + { + // TODO : pay the fees to the correct (dividend-distribution) account + asset fee = asset(fee_collected_pair.second, fee_collected_pair.first); + adjust_balance(account_id_type(), fee); + fees.push_back(fee); + } + push_applied_operation(betting_market_group_resolved_operation(bettor_id, + betting_market_group.id, + resolutions, + winnings, + fees)); + } - push_applied_operation(betting_market_resolved_operation(position.bettor_id, - betting_market.id, - resolution, - payout_amount, - position.fees_collected)); - - remove(position); + betting_market_itr = betting_market_index.lower_bound(betting_market_group.id); + while (betting_market_itr != betting_market_index.end() && betting_market_itr->group_id == betting_market_group.id) + { + const betting_market_object& betting_market = *betting_market_itr; + ++betting_market_itr; + remove(betting_market); } - remove(betting_market); + remove(betting_market_group); } #if 0 diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index e37578aa..a57af12d 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -218,7 +218,7 @@ void database::initialize_evaluators() register_evaluator(); register_evaluator(); register_evaluator(); - register_evaluator(); + register_evaluator(); } void database::initialize_indexes() diff --git a/libraries/chain/db_notify.cpp b/libraries/chain/db_notify.cpp index a03c7c7b..a3660bc9 100644 --- a/libraries/chain/db_notify.cpp +++ b/libraries/chain/db_notify.cpp @@ -192,8 +192,8 @@ struct get_impacted_account_visitor void operator()(const betting_market_group_create_operation&){} void operator()(const betting_market_create_operation&){} void operator()(const bet_place_operation&){} - void operator()(const betting_market_resolve_operation&){} - void operator()(const betting_market_resolved_operation &){} + void operator()(const betting_market_group_resolve_operation&){} + void operator()(const betting_market_group_resolved_operation &){} void operator()(const bet_matched_operation &){} void operator()(const bet_cancel_operation&){} void operator()(const bet_canceled_operation &){} diff --git a/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp b/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp index bae04611..f25e54f6 100644 --- a/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp +++ b/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp @@ -76,15 +76,16 @@ namespace graphene { namespace chain { const bet_object* _bet_to_cancel; }; - class betting_market_resolve_evaluator : public evaluator + class betting_market_group_resolve_evaluator : public evaluator { public: - typedef betting_market_resolve_operation operation_type; + typedef betting_market_group_resolve_operation operation_type; - void_result do_evaluate( const betting_market_resolve_operation& o ); - void_result do_apply( const betting_market_resolve_operation& o ); + void_result do_evaluate( const betting_market_group_resolve_operation& o ); + void_result do_apply( const betting_market_group_resolve_operation& o ); private: - const betting_market_object* _betting_market; + const betting_market_group_object* _betting_market_group; }; + } } // graphene::chain diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index a37449c0..b06c60b9 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -375,8 +375,10 @@ namespace graphene { namespace chain { void cancel_bet(const bet_object& bet, bool create_virtual_op = true); void cancel_all_unmatched_bets_on_betting_market(const betting_market_object& betting_market); void cancel_all_betting_markets_for_event(const event_object&); - void resolve_betting_market(const betting_market_object& betting_market, - betting_market_resolution_type resolution); + void validate_betting_market_group_resolutions(const betting_market_group_object& betting_market_group, + const std::map& resolutions); + void resolve_betting_market_group(const betting_market_group_object& betting_market_group, + const std::map& resolutions); /** * @brief Process a new bet * @param new_bet_object The new bet to process diff --git a/libraries/chain/include/graphene/chain/protocol/betting_market.hpp b/libraries/chain/include/graphene/chain/protocol/betting_market.hpp index 5bedf242..5996f4f8 100644 --- a/libraries/chain/include/graphene/chain/protocol/betting_market.hpp +++ b/libraries/chain/include/graphene/chain/protocol/betting_market.hpp @@ -95,14 +95,14 @@ enum class betting_market_resolution_type { BETTING_MARKET_RESOLUTION_COUNT }; -struct betting_market_resolve_operation : public base_operation +struct betting_market_group_resolve_operation : public base_operation { struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; asset fee; - betting_market_id_type betting_market_id; + betting_market_group_id_type betting_market_group_id; - betting_market_resolution_type resolution; + std::map resolutions; extensions_type extensions; @@ -110,30 +110,32 @@ struct betting_market_resolve_operation : public base_operation void validate()const; }; -struct betting_market_resolved_operation : public base_operation +struct betting_market_group_resolved_operation : public base_operation { struct fee_parameters_type {}; account_id_type bettor_id; - betting_market_id_type betting_market_id; - betting_market_resolution_type resolution; - asset winnings; - share_type fees_paid; + betting_market_group_id_type betting_market_group_id; + std::map resolutions; + std::vector winnings; + std::vector fees_paid; asset fee; // unused in a virtual operation - betting_market_resolved_operation() {} - betting_market_resolved_operation(account_id_type bettor_id, - betting_market_id_type betting_market_id, - betting_market_resolution_type resolution, - asset winnings, - share_type fees_paid) : + betting_market_group_resolved_operation() {} + betting_market_group_resolved_operation(account_id_type bettor_id, + betting_market_group_id_type betting_market_group_id, + const std::map& resolutions, + std::vector winnings, + std::vector fees_paid) : bettor_id(bettor_id), - betting_market_id(betting_market_id), - resolution(resolution), + betting_market_group_id(betting_market_group_id), + resolutions(resolutions), winnings(winnings), fees_paid(fees_paid) - {} + { + // TODO ? + } account_id_type fee_payer()const { return bettor_id; } void validate()const { FC_ASSERT(false, "virtual operation"); } @@ -279,13 +281,13 @@ FC_REFLECT( graphene::chain::betting_market_create_operation, FC_REFLECT_ENUM( graphene::chain::betting_market_resolution_type, (win)(not_win)(cancel)(BETTING_MARKET_RESOLUTION_COUNT) ) -FC_REFLECT( graphene::chain::betting_market_resolve_operation::fee_parameters_type, (fee) ) -FC_REFLECT( graphene::chain::betting_market_resolve_operation, - (fee)(betting_market_id)(resolution)(extensions) ) +FC_REFLECT( graphene::chain::betting_market_group_resolve_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::betting_market_group_resolve_operation, + (fee)(betting_market_group_id)(resolutions)(extensions) ) -FC_REFLECT( graphene::chain::betting_market_resolved_operation::fee_parameters_type, ) -FC_REFLECT( graphene::chain::betting_market_resolved_operation, - (bettor_id)(betting_market_id)(resolution)(winnings)(fees_paid)(fee) ) +FC_REFLECT( graphene::chain::betting_market_group_resolved_operation::fee_parameters_type, ) +FC_REFLECT( graphene::chain::betting_market_group_resolved_operation, + (bettor_id)(betting_market_group_id)(resolutions)(winnings)(fees_paid)(fee) ) FC_REFLECT_ENUM( graphene::chain::bet_type, (back)(lay) ) FC_REFLECT( graphene::chain::bet_place_operation::fee_parameters_type, (fee) ) diff --git a/libraries/chain/include/graphene/chain/protocol/operations.hpp b/libraries/chain/include/graphene/chain/protocol/operations.hpp index 8ba64fb7..097ad854 100644 --- a/libraries/chain/include/graphene/chain/protocol/operations.hpp +++ b/libraries/chain/include/graphene/chain/protocol/operations.hpp @@ -104,8 +104,8 @@ namespace graphene { namespace chain { betting_market_group_create_operation, betting_market_create_operation, bet_place_operation, - betting_market_resolve_operation, - betting_market_resolved_operation, // VIRTUAL + betting_market_group_resolve_operation, + betting_market_group_resolved_operation, // VIRTUAL bet_matched_operation, // VIRTUAL bet_cancel_operation, bet_canceled_operation // VIRTUAL diff --git a/libraries/chain/protocol/betting_market.cpp b/libraries/chain/protocol/betting_market.cpp index b0578c53..405e5c16 100644 --- a/libraries/chain/protocol/betting_market.cpp +++ b/libraries/chain/protocol/betting_market.cpp @@ -35,9 +35,9 @@ void betting_market_create_operation::validate() const FC_ASSERT( fee.amount >= 0 ); } -void betting_market_resolve_operation::validate() const +void betting_market_group_resolve_operation::validate() const { - FC_ASSERT( fee.amount >= 0 ); + //FC_ASSERT( fee.amount >= 0 ); } void bet_place_operation::validate() const diff --git a/libraries/wallet/include/graphene/wallet/wallet.hpp b/libraries/wallet/include/graphene/wallet/wallet.hpp index 13d29f9f..84b17dea 100644 --- a/libraries/wallet/include/graphene/wallet/wallet.hpp +++ b/libraries/wallet/include/graphene/wallet/wallet.hpp @@ -1559,11 +1559,11 @@ class wallet_api share_type amount_reserved_for_fees, bool broadcast = false); - signed_transaction propose_resolve_betting_market( + signed_transaction propose_resolve_betting_market_group( const string& proposing_account, fc::time_point_sec expiration_time, - betting_market_id_type betting_market_id, - betting_market_resolution_type resolution, + betting_market_group_id_type betting_market_group_id, + const std::map& resolutions, bool broadcast = false); void dbg_make_uia(string creator, string symbol); @@ -1779,6 +1779,6 @@ FC_API( graphene::wallet::wallet_api, (propose_create_betting_market_group) (propose_create_betting_market) (place_bet) - (propose_resolve_betting_market) + (propose_resolve_betting_market_group) (get_order_book) ) diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index 0681c649..adadf868 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -4577,25 +4577,25 @@ signed_transaction wallet_api::place_bet( return my->sign_transaction(tx, broadcast); } -signed_transaction wallet_api::propose_resolve_betting_market( +signed_transaction wallet_api::propose_resolve_betting_market_group( const string& proposing_account, fc::time_point_sec expiration_time, - betting_market_id_type betting_market_id, - betting_market_resolution_type resolution, + betting_market_group_id_type betting_market_group_id, + const std::map& resolutions, bool broadcast /*= false*/) { FC_ASSERT( !is_locked() ); const chain_parameters& current_params = get_global_properties().parameters; - betting_market_resolve_operation betting_market_resolve_op; - betting_market_resolve_op.betting_market_id = betting_market_id; - betting_market_resolve_op.resolution = resolution; + betting_market_group_resolve_operation betting_market_group_resolve_op; + betting_market_group_resolve_op.betting_market_group_id = betting_market_group_id; + betting_market_group_resolve_op.resolutions = resolutions; proposal_create_operation prop_op; prop_op.expiration_time = expiration_time; prop_op.review_period_seconds = current_params.committee_proposal_review_period; prop_op.fee_paying_account = get_account(proposing_account).id; - prop_op.proposed_ops.emplace_back( betting_market_resolve_op ); + prop_op.proposed_ops.emplace_back( betting_market_group_resolve_op ); current_params.current_fees->set_fee( prop_op.proposed_ops.back().op ); signed_transaction tx; diff --git a/tests/betting/betting_tests.cpp b/tests/betting/betting_tests.cpp index 7ca41dac..c8d56b6c 100644 --- a/tests/betting/betting_tests.cpp +++ b/tests/betting/betting_tests.cpp @@ -114,7 +114,9 @@ BOOST_AUTO_TEST_CASE( peerplays_sport_create_test ) BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000 - 20000); // caps win - resolve_betting_market(capitals_win_market.id, betting_market_resolution_type::win); + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::cancel}}); BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 - 20000 + 2000000); BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000 - 20000); @@ -253,6 +255,9 @@ BOOST_AUTO_TEST_SUITE_END() // the result in all three possible outcomes struct simple_bet_test_fixture : database_fixture { betting_market_id_type capitals_win_betting_market_id; + betting_market_id_type blackhawks_win_betting_market_id; + betting_market_group_id_type moneyline_betting_markets_id; + simple_bet_test_fixture() { ACTORS( (alice)(bob) ); @@ -271,6 +276,8 @@ struct simple_bet_test_fixture : database_fixture { place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(1100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION, 22); capitals_win_betting_market_id = capitals_win_market.id; + blackhawks_win_betting_market_id = blackhawks_win_market.id; + moneyline_betting_markets_id = moneyline_betting_markets.id; } }; @@ -280,7 +287,9 @@ BOOST_AUTO_TEST_CASE( win ) { try { - resolve_betting_market(capitals_win_betting_market_id, betting_market_resolution_type::win); + resolve_betting_market_group(moneyline_betting_markets_id, + {{capitals_win_betting_market_id, betting_market_resolution_type::win}, + {blackhawks_win_betting_market_id, betting_market_resolution_type::cancel}}); GET_ACTOR(alice); GET_ACTOR(bob); @@ -296,7 +305,9 @@ BOOST_AUTO_TEST_CASE( not_win ) { try { - resolve_betting_market(capitals_win_betting_market_id, betting_market_resolution_type::not_win); + resolve_betting_market_group(moneyline_betting_markets_id, + {{capitals_win_betting_market_id, betting_market_resolution_type::not_win}, + {blackhawks_win_betting_market_id, betting_market_resolution_type::cancel}}); GET_ACTOR(alice); GET_ACTOR(bob); @@ -312,7 +323,9 @@ BOOST_AUTO_TEST_CASE( cancel ) { try { - resolve_betting_market(capitals_win_betting_market_id, betting_market_resolution_type::cancel); + resolve_betting_market_group(moneyline_betting_markets_id, + {{capitals_win_betting_market_id, betting_market_resolution_type::cancel}, + {blackhawks_win_betting_market_id, betting_market_resolution_type::cancel}}); GET_ACTOR(alice); GET_ACTOR(bob); diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index 308512d9..04fb62ec 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -1198,13 +1198,14 @@ const betting_market_object& database_fixture::create_betting_market(betting_mar trx.operations.clear(); } FC_CAPTURE_AND_RETHROW( (bettor_id)(back_or_lay)(amount_to_bet) ) } -void database_fixture::resolve_betting_market(betting_market_id_type betting_market_id, betting_market_resolution_type resolution) +void database_fixture::resolve_betting_market_group(betting_market_group_id_type betting_market_group_id, + std::map resolutions) { try { - betting_market_resolve_operation betting_market_resolve_op; - betting_market_resolve_op.betting_market_id = betting_market_id; - betting_market_resolve_op.resolution = resolution; - process_operation_by_witnesses(betting_market_resolve_op); -} FC_CAPTURE_AND_RETHROW( (betting_market_id)(resolution) ) } + betting_market_group_resolve_operation betting_market_group_resolve_op; + betting_market_group_resolve_op.betting_market_group_id = betting_market_group_id; + betting_market_group_resolve_op.resolutions = resolutions; + process_operation_by_witnesses(betting_market_group_resolve_op); +} FC_CAPTURE_AND_RETHROW( (betting_market_group_id)(resolutions) ) } namespace test { diff --git a/tests/common/database_fixture.hpp b/tests/common/database_fixture.hpp index e7041eea..de800fe0 100644 --- a/tests/common/database_fixture.hpp +++ b/tests/common/database_fixture.hpp @@ -290,7 +290,7 @@ struct database_fixture { const betting_market_object& create_betting_market(betting_market_group_id_type group_id, internationalized_string_type payout_condition, asset_id_type asset_id); void place_bet(account_id_type bettor_id, betting_market_id_type betting_market_id, bet_type back_or_lay, asset amount_to_bet, bet_multiplier_type backer_multiplier, share_type amount_reserved_for_fees); - void resolve_betting_market(betting_market_id_type betting_market_id, betting_market_resolution_type resolution); + void resolve_betting_market_group(betting_market_group_id_type betting_market_group_id, std::map resolutions); }; namespace test { From b9304caffa8b737dfb055a2991c9dfc6d668316d Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Sun, 26 Jun 2016 15:41:07 -0400 Subject: [PATCH 02/19] Cherry-picked commit abc7853. Initial work on dividend-paying assets. Basic functionality works in simple cases. --- libraries/app/impacted.cpp | 1 + libraries/chain/asset_evaluator.cpp | 61 ++++ libraries/chain/db_init.cpp | 5 +- libraries/chain/db_maint.cpp | 287 ++++++++++++++++++ .../include/graphene/chain/account_object.hpp | 55 ++++ .../graphene/chain/asset_evaluator.hpp | 12 + .../include/graphene/chain/asset_object.hpp | 86 ++++++ .../graphene/chain/protocol/asset_ops.hpp | 68 +++++ .../graphene/chain/protocol/operations.hpp | 3 +- .../include/graphene/chain/protocol/types.hpp | 12 +- libraries/chain/protocol/asset_ops.cpp | 10 + libraries/wallet/wallet.cpp | 25 +- tests/common/database_fixture.cpp | 18 ++ tests/common/database_fixture.hpp | 3 + tests/tests/operation_tests.cpp | 222 ++++++++++++++ 15 files changed, 863 insertions(+), 5 deletions(-) diff --git a/libraries/app/impacted.cpp b/libraries/app/impacted.cpp index 7a5370dc..b5dbd979 100644 --- a/libraries/app/impacted.cpp +++ b/libraries/app/impacted.cpp @@ -90,6 +90,7 @@ struct get_impacted_account_visitor } void operator()( const asset_update_bitasset_operation& op ) {} + void operator()( const asset_update_dividend_operation& op ) {} void operator()( const asset_update_feed_producers_operation& op ) {} void operator()( const asset_issue_operation& op ) diff --git a/libraries/chain/asset_evaluator.cpp b/libraries/chain/asset_evaluator.cpp index 1f2826a0..16b6ee18 100644 --- a/libraries/chain/asset_evaluator.cpp +++ b/libraries/chain/asset_evaluator.cpp @@ -32,6 +32,8 @@ #include +#include + namespace graphene { namespace chain { void_result asset_create_evaluator::do_evaluate( const asset_create_operation& op ) @@ -366,6 +368,65 @@ void_result asset_update_bitasset_evaluator::do_apply(const asset_update_bitasse return void_result(); } FC_CAPTURE_AND_RETHROW( (o) ) } +void_result asset_update_dividend_evaluator::do_evaluate(const asset_update_dividend_operation& o) +{ try { + database& d = db(); + + const asset_object& a = o.asset_to_update(d); + asset_to_update = &a; + + FC_ASSERT( o.issuer == a.issuer, "", ("o.issuer", o.issuer)("a.issuer", a.issuer) ); + auto& params = db().get_global_properties().parameters; + if (o.new_options.payout_interval && + *o.new_options.payout_interval < params.maintenance_interval) + FC_THROW("New payout interval may not be less than the maintenance interval", + ("new_payout_interval", o.new_options.payout_interval)("maintenance_interval", params.maintenance_interval)); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (o) ) } + +void_result asset_update_dividend_evaluator::do_apply( const asset_update_dividend_operation& op ) +{ try { + database& d = db(); + if (!asset_to_update->dividend_data_id) + { + // this was not a dividend-paying asset, we're converting it to a dividend-paying asset + std::string dividend_distribution_account_name(boost::to_lower_copy(asset_to_update->symbol) + "-dividend-distribution"); + + const auto& new_acnt_object = db().create( [&]( account_object& obj ){ + obj.registrar = op.issuer; + obj.referrer = op.issuer; + obj.lifetime_referrer = op.issuer(db()).lifetime_referrer; + + auto& params = db().get_global_properties().parameters; + obj.network_fee_percentage = GRAPHENE_DEFAULT_NETWORK_PERCENT_OF_FEE; + obj.lifetime_referrer_fee_percentage = GRAPHENE_DEFAULT_LIFETIME_REFERRER_PERCENT_OF_FEE; + obj.referrer_rewards_percentage = GRAPHENE_DEFAULT_LIFETIME_REFERRER_PERCENT_OF_FEE; + + obj.name = dividend_distribution_account_name; + obj.owner.weight_threshold = 1; + obj.active.weight_threshold = 1; + obj.statistics = db().create([&](account_statistics_object& s){s.owner = obj.id;}).id; + }); + + const asset_dividend_data_object& dividend_data = d.create( [&]( asset_dividend_data_object& dividend_data_obj ) { + dividend_data_obj.options = op.new_options; + dividend_data_obj.dividend_distribution_account = new_acnt_object.id; + }); + + d.modify(*asset_to_update, [&](asset_object& a) { + a.dividend_data_id = dividend_data.id; + }); + } + else + { + const asset_dividend_data_object& dividend_data = asset_to_update->dividend_data(d); + d.modify(dividend_data, [&]( asset_dividend_data_object& dividend_data_obj ) { + dividend_data_obj.options = op.new_options; + }); + } + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + void_result asset_update_feed_producers_evaluator::do_evaluate(const asset_update_feed_producers_evaluator::operation_type& o) { try { database& d = db(); diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index a57af12d..d19ce204 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -183,6 +183,7 @@ void database::initialize_evaluators() register_evaluator(); register_evaluator(); register_evaluator(); + register_evaluator(); register_evaluator(); register_evaluator(); register_evaluator(); @@ -259,6 +260,7 @@ void database::initialize_indexes() add_index< primary_index >(); add_index< primary_index >(); add_index< primary_index >(); + add_index< primary_index >(); add_index< primary_index> >(); add_index< primary_index> >(); add_index< primary_index> >(); @@ -269,10 +271,11 @@ void database::initialize_indexes() add_index< primary_index > >(); add_index< primary_index< special_authority_index > >(); add_index< primary_index< buyback_index > >(); - add_index< primary_index< simple_index< fba_accumulator_object > > >(); add_index< primary_index< betting_market_position_index > >(); add_index< primary_index< global_betting_statistics_object_index > >(); + add_index< primary_index >(); + add_index< primary_index >(); } void database::init_genesis(const genesis_state_type& genesis_state) diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index d515a961..6bef6051 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -716,6 +716,291 @@ void deprecate_annual_members( database& db ) return; } +void schedule_pending_dividend_balances(database& db, + const asset_object& dividend_holder_asset_obj, + const asset_dividend_data_object& dividend_data, + const account_balance_index& balance_index, + const distributed_dividend_balance_object_index& distributed_dividend_balance_index, + const pending_dividend_payout_balance_object_index& pending_payout_balance_index) +{ + dlog("Processing dividend payments for dividend holder asset asset type ${holder_asset}", ("holder_asset", dividend_holder_asset_obj.symbol)); + auto current_distribution_account_balance_range = + balance_index.indices().get().equal_range(boost::make_tuple(dividend_data.dividend_distribution_account)); + auto previous_distribution_account_balance_range = + distributed_dividend_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id)); + // the current range is now all current balances for the distribution account, sorted by asset_type + // the previous range is now all previous balances for this account, sorted by asset type + + auto current_distribution_account_balance_iter = current_distribution_account_balance_range.first; + auto previous_distribution_account_balance_iter = previous_distribution_account_balance_range.first; + dlog("Current balances in distribution account: ${current}, Previous balances: ${previous}", + ("current", std::distance(current_distribution_account_balance_range.first, current_distribution_account_balance_range.second)) + ("previous", std::distance(previous_distribution_account_balance_range.first, previous_distribution_account_balance_range.second))); + + while (current_distribution_account_balance_iter != current_distribution_account_balance_range.second || + previous_distribution_account_balance_iter != previous_distribution_account_balance_range.second) + { + share_type current_balance; + share_type previous_balance; + asset_id_type payout_asset_type; + + if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || + current_distribution_account_balance_iter->asset_type < previous_distribution_account_balance_iter->dividend_payout_asset_type) + { + payout_asset_type = current_distribution_account_balance_iter->asset_type; + current_balance = current_distribution_account_balance_iter->balance; + idump((payout_asset_type)(current_balance)); + } + else if (current_distribution_account_balance_iter == current_distribution_account_balance_range.second || + previous_distribution_account_balance_iter->dividend_payout_asset_type < current_distribution_account_balance_iter->asset_type) + { + payout_asset_type = previous_distribution_account_balance_iter->dividend_payout_asset_type; + previous_balance = previous_distribution_account_balance_iter->balance_at_last_maintenance_interval; + idump((payout_asset_type)(previous_balance)); + } + else + { + payout_asset_type = current_distribution_account_balance_iter->asset_type; + current_balance = current_distribution_account_balance_iter->balance; + previous_balance = previous_distribution_account_balance_iter->balance_at_last_maintenance_interval; + idump((payout_asset_type)(current_balance)(previous_balance)); + } + + share_type delta_balance = current_balance - previous_balance; + dlog("Processing dividend payments of asset type ${payout_asset_type}, delta balance is ${delta_balance}", ("payout_asset_type", payout_asset_type(db).symbol)("delta_balance", delta_balance)); + if (delta_balance > 0) + { + // we need to pay out delta_balance to shareholders proportional to their stake + auto holder_account_balance_range = + balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id)); + share_type total_balance_of_dividend_asset; + for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_account_balance_range.first, holder_account_balance_range.second)) + if (holder_balance_object.owner != dividend_data.dividend_distribution_account) + // TODO: if holder_balance_object.owner is able to accept payout_asset_type + total_balance_of_dividend_asset += holder_balance_object.balance; + + dlog("There are ${count} holders of the dividend-paying asset, with a total balance of ${total}", + ("count", std::distance(holder_account_balance_range.first, holder_account_balance_range.second)) + ("total", total_balance_of_dividend_asset)); + share_type remaining_amount_to_distribute = delta_balance; + share_type remaining_balance_of_dividend_asset = total_balance_of_dividend_asset; + + for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_account_balance_range.first, holder_account_balance_range.second)) + if (holder_balance_object.owner != dividend_data.dividend_distribution_account) + { + // TODO: if holder_balance_object.owner is able to accept payout_asset_type + fc::uint128_t amount_to_credit(remaining_amount_to_distribute.value); + amount_to_credit *= holder_balance_object.balance.value; + amount_to_credit /= remaining_balance_of_dividend_asset.value; + share_type shares_to_credit((int64_t)amount_to_credit.to_uint64()); + + remaining_amount_to_distribute -= shares_to_credit; + remaining_balance_of_dividend_asset -= holder_balance_object.balance; + + dlog("Crediting account ${account} with ${amount}", ("account", holder_balance_object.owner(db).name)("amount", amount_to_credit)); + auto pending_payout_iter = + pending_payout_balance_index.indices().get().find(boost::make_tuple(dividend_holder_asset_obj.id, payout_asset_type, holder_balance_object.owner)); + if (pending_payout_iter == pending_payout_balance_index.indices().get().end()) + db.create( [&]( pending_dividend_payout_balance_object& obj ){ + obj.owner = holder_balance_object.owner; + obj.dividend_holder_asset_type = dividend_holder_asset_obj.id; + obj.dividend_payout_asset_type = payout_asset_type; + obj.pending_balance = shares_to_credit; + }); + else + db.modify(*pending_payout_iter, [&]( pending_dividend_payout_balance_object& pending_balance ){ + pending_balance.pending_balance += shares_to_credit; + }); + } + + for (const auto& pending_payout : pending_payout_balance_index.indices()) + { + dlog("Pending payout: ${account_name} -> ${amount}", ("account_name", pending_payout.owner(db).name)("amount", pending_payout.pending_balance)); + } + + share_type distributed_amount = current_balance - remaining_amount_to_distribute; + if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || + previous_distribution_account_balance_iter->dividend_payout_asset_type != payout_asset_type) + db.create( [&]( distributed_dividend_balance_object& obj ){ + obj.dividend_holder_asset_type = dividend_holder_asset_obj.id; + obj.dividend_payout_asset_type = payout_asset_type; + obj.balance_at_last_maintenance_interval = distributed_amount; + }); + else + db.modify(*previous_distribution_account_balance_iter, [&]( distributed_dividend_balance_object& obj ){ + obj.balance_at_last_maintenance_interval = distributed_amount; + }); + } + else if (delta_balance < 0) + { + // some amount of the asset has been withdrawn from the dividend_distribution_account, + // meaning the current pending payout balances will add up to more than our current balance. + // This should be extremely rare. + // Reduce all pending payouts proportionally + share_type total_pending_balances; + auto pending_payouts_range = + pending_payout_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id, payout_asset_type)); + + for (const pending_dividend_payout_balance_object& pending_balance_object : boost::make_iterator_range(pending_payouts_range.first, pending_payouts_range.second)) + total_pending_balances += pending_balance_object.pending_balance; + + share_type remaining_amount_to_recover = -delta_balance; + share_type remaining_pending_balances = total_pending_balances; + for (const pending_dividend_payout_balance_object& pending_balance_object : boost::make_iterator_range(pending_payouts_range.first, pending_payouts_range.second)) + { + fc::uint128_t amount_to_debit(remaining_amount_to_recover.value); + amount_to_debit *= pending_balance_object.pending_balance.value; + amount_to_debit /= remaining_pending_balances.value; + share_type shares_to_debit((int64_t)amount_to_debit.to_uint64()); + + remaining_amount_to_recover -= shares_to_debit; + remaining_pending_balances -= pending_balance_object.pending_balance; + + db.modify(pending_balance_object, [&]( pending_dividend_payout_balance_object& pending_balance ){ + pending_balance.pending_balance -= shares_to_debit; + }); + } + + if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || + previous_distribution_account_balance_iter->dividend_payout_asset_type != payout_asset_type) + db.create( [&]( distributed_dividend_balance_object& obj ){ + obj.dividend_holder_asset_type = dividend_holder_asset_obj.id; + obj.dividend_payout_asset_type = payout_asset_type; + obj.balance_at_last_maintenance_interval = 0; + }); + else + db.modify(*previous_distribution_account_balance_iter, [&]( distributed_dividend_balance_object& obj ){ + obj.balance_at_last_maintenance_interval = 0; + }); + } + + // iterate + if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || + current_distribution_account_balance_iter->asset_type < previous_distribution_account_balance_iter->dividend_payout_asset_type) + ++current_distribution_account_balance_iter; + else if (current_distribution_account_balance_iter == current_distribution_account_balance_range.second || + previous_distribution_account_balance_iter->dividend_payout_asset_type < current_distribution_account_balance_iter->asset_type) + ++previous_distribution_account_balance_iter; + else + { + ++current_distribution_account_balance_iter; + ++previous_distribution_account_balance_iter; + } + } +} + +void process_dividend_assets(database& db) +{ + ilog("In process_dividend_assets time ${time}", ("time", db.head_block_time())); + + const account_balance_index& balance_index = db.get_index_type(); + const distributed_dividend_balance_object_index& distributed_dividend_balance_index = db.get_index_type(); + const pending_dividend_payout_balance_object_index& pending_payout_balance_index = db.get_index_type(); + + // TODO: switch to iterating over only dividend assets (generalize the by_type index) + for( const asset_object& dividend_holder_asset_obj : db.get_index_type().indices() ) + if (dividend_holder_asset_obj.dividend_data_id) + { + const asset_dividend_data_object& dividend_data = dividend_holder_asset_obj.dividend_data(db); + schedule_pending_dividend_balances(db, dividend_holder_asset_obj, dividend_data, + balance_index, distributed_dividend_balance_index, pending_payout_balance_index); + fc::time_point_sec current_head_block_time = db.head_block_time(); + if (dividend_data.options.next_payout_time && + db.head_block_time() >= *dividend_data.options.next_payout_time) + { + dlog("Dividend payout time has arrived for asset ${holder_asset}", + ("holder_asset", dividend_holder_asset_obj.symbol)); + +#ifndef NDEBUG + // dump balances before the payouts for debugging + const auto& balance_idx = db.get_index_type().indices().get(); + auto holder_account_balance_range = balance_idx.equal_range(boost::make_tuple(dividend_data.dividend_distribution_account)); + for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_account_balance_range.first, holder_account_balance_range.second)) + ilog(" Current balance: ${asset}", ("asset", asset(holder_balance_object.balance, holder_balance_object.asset_type))); +#endif + + // when we do the payouts, we first increase the balances in all of the receiving accounts + // and use this map to keep track of the total amount of each asset paid out. + // Afterwards, we decrease the distribution account's balance by the total amount paid out, + // and modify the distributed_balances accordingly +#ifndef NDEBUG + // for debugging, sum up our payouts here + std::map amounts_paid_out_by_asset; +#endif + auto pending_payouts_range = + pending_payout_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id)); + for (auto pending_balance_object_iter = pending_payouts_range.first; pending_balance_object_iter != pending_payouts_range.second; ) + { + const pending_dividend_payout_balance_object& pending_balance_object = *pending_balance_object_iter; + ilog("Processing payout of ${asset} to account ${account}", + ("asset", asset(pending_balance_object.pending_balance, pending_balance_object.dividend_payout_asset_type)) + ("account", pending_balance_object.owner(db).name)); + + db.adjust_balance(pending_balance_object.owner, + asset(pending_balance_object.pending_balance, + pending_balance_object.dividend_payout_asset_type)); +#ifndef NDEBUG + amounts_paid_out_by_asset[pending_balance_object.dividend_payout_asset_type] += pending_balance_object.pending_balance; +#endif + + ++pending_balance_object_iter; + db.remove(pending_balance_object); + } + + // now debit the total amount of dividends paid out from the distribution account + auto distributed_balance_range = + distributed_dividend_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id)); + +#ifndef NDEBUG + // validate that we actually paid out exactly as much as we had planned to + assert(amounts_paid_out_by_asset.size() == std::distance(distributed_balance_range.first, distributed_balance_range.second)); + if (amounts_paid_out_by_asset.size() == std::distance(distributed_balance_range.first, distributed_balance_range.second)) + { + auto distributed_balance_object_iter = distributed_balance_range.first; + for (const auto& asset_and_amount_paid_out : amounts_paid_out_by_asset) + { + assert(distributed_balance_object_iter->dividend_payout_asset_type == asset_and_amount_paid_out.first); + assert(distributed_balance_object_iter->balance_at_last_maintenance_interval == asset_and_amount_paid_out.second); + ++distributed_balance_object_iter; + } + } +#endif + + for (auto distributed_balance_object_iter = distributed_balance_range.first; distributed_balance_object_iter != distributed_balance_range.second; ) + { + const distributed_dividend_balance_object& distributed_balance_object = *distributed_balance_object_iter; + db.adjust_balance(dividend_data.dividend_distribution_account, + asset(-distributed_balance_object.balance_at_last_maintenance_interval, + distributed_balance_object.dividend_payout_asset_type)); + ++distributed_balance_object_iter; + db.remove(distributed_balance_object); // now they've been paid out, reset to zero + } + + // now schedule the next payout time + db.modify(dividend_data, [current_head_block_time](asset_dividend_data_object& dividend_data_obj) { + dlog("Updating dividend payout time, new values are:"); + dividend_data_obj.last_scheduled_payout_time = dividend_data_obj.options.next_payout_time; + dividend_data_obj.last_payout_time = current_head_block_time; + fc::optional next_payout_time; + if (dividend_data_obj.options.payout_interval) + { + // if there was a previous payout, make our next payment one interval + uint32_t current_time_sec = current_head_block_time.sec_since_epoch(); + fc::time_point_sec reference_time = *dividend_data_obj.last_scheduled_payout_time; + uint32_t next_possible_time_sec = dividend_data_obj.last_scheduled_payout_time->sec_since_epoch(); + do + next_possible_time_sec += *dividend_data_obj.options.payout_interval; + while (next_possible_time_sec <= current_time_sec); + + next_payout_time = next_possible_time_sec; + } + dividend_data_obj.options.next_payout_time = next_payout_time; + idump((dividend_data_obj.last_scheduled_payout_time)(dividend_data_obj.last_payout_time)(dividend_data_obj.options.next_payout_time)); + }); + } + } +} + void database::perform_chain_maintenance(const signed_block& next_block, const global_property_object& global_props) { const auto& gpo = get_global_properties(); @@ -723,6 +1008,8 @@ void database::perform_chain_maintenance(const signed_block& next_block, const g distribute_fba_balances(*this); create_buyback_orders(*this); + process_dividend_assets(*this); + struct vote_tally_helper { database& d; const global_property_object& props; diff --git a/libraries/chain/include/graphene/chain/account_object.hpp b/libraries/chain/include/graphene/chain/account_object.hpp index 522cb7bc..d7ba8b34 100644 --- a/libraries/chain/include/graphene/chain/account_object.hpp +++ b/libraries/chain/include/graphene/chain/account_object.hpp @@ -311,6 +311,31 @@ namespace graphene { namespace chain { /** maps the referrer to the set of accounts that they have referred */ map< account_id_type, set > referred_by; }; + + /** + * @brief Tracks a pending payout of a single dividend payout asset + * from a single dividend holder asset to a holder's account. + * + * Each maintenance interval, this will be adjusted to account for + * any new transfers to the dividend distribution account. + * @ingroup object + * + */ + class pending_dividend_payout_balance_object : public abstract_object + { + public: + static const uint8_t space_id = implementation_ids; + static const uint8_t type_id = impl_pending_dividend_payout_balance_object_type; + + account_id_type owner; + asset_id_type dividend_holder_asset_type; + asset_id_type dividend_payout_asset_type; + share_type pending_balance; + + asset get_pending_balance()const { return asset(pending_balance, dividend_payout_asset_type); } + void adjust_balance(const asset& delta); + }; + struct by_account_asset; struct by_asset_balance; @@ -367,6 +392,31 @@ namespace graphene { namespace chain { */ typedef generic_index account_index; + struct by_dividend_asset_account_asset{}; + + /** + * @ingroup object_index + */ + typedef multi_index_container< + pending_dividend_payout_balance_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > >, + ordered_unique< tag, + composite_key< + pending_dividend_payout_balance_object, + member, + member, + member + > + > + > + > pending_dividend_payout_balance_object_multi_index_type; + + /** + * @ingroup object_index + */ + typedef generic_index pending_dividend_payout_balance_object_index; + }} FC_REFLECT_DERIVED( graphene::chain::account_object, @@ -395,3 +445,8 @@ FC_REFLECT_DERIVED( graphene::chain::account_statistics_object, (pending_fees)(pending_vested_fees) ) +FC_REFLECT_DERIVED( graphene::chain::pending_dividend_payout_balance_object, + (graphene::db::object), + (owner)(dividend_holder_asset_type)(dividend_payout_asset_type)(pending_balance) ) + + diff --git a/libraries/chain/include/graphene/chain/asset_evaluator.hpp b/libraries/chain/include/graphene/chain/asset_evaluator.hpp index eb8d5789..234a60d7 100644 --- a/libraries/chain/include/graphene/chain/asset_evaluator.hpp +++ b/libraries/chain/include/graphene/chain/asset_evaluator.hpp @@ -82,6 +82,18 @@ namespace graphene { namespace chain { const asset_bitasset_data_object* bitasset_to_update = nullptr; }; + class asset_update_dividend_evaluator : public evaluator + { + public: + typedef asset_update_dividend_operation operation_type; + + void_result do_evaluate( const asset_update_dividend_operation& o ); + void_result do_apply( const asset_update_dividend_operation& o ); + + const asset_object* asset_to_update = nullptr; + const asset_dividend_data_object* asset_dividend_data_to_update = nullptr; + }; + class asset_update_feed_producers_evaluator : public evaluator { public: diff --git a/libraries/chain/include/graphene/chain/asset_object.hpp b/libraries/chain/include/graphene/chain/asset_object.hpp index d93993fe..f41afbe1 100644 --- a/libraries/chain/include/graphene/chain/asset_object.hpp +++ b/libraries/chain/include/graphene/chain/asset_object.hpp @@ -132,6 +132,9 @@ namespace graphene { namespace chain { optional buyback_account; + /// Extra data associated with dividend-paying assets. + optional dividend_data_id; + asset_id_type get_id()const { return id; } void validate()const @@ -148,6 +151,10 @@ namespace graphene { namespace chain { const asset_bitasset_data_object& bitasset_data(const DB& db)const { assert(bitasset_data_id); return db.get(*bitasset_data_id); } + template + const asset_dividend_data_object& dividend_data(const DB& db)const + { assert(dividend_data_id); return db.get(*dividend_data_id); } + template const asset_dynamic_data_object& dynamic_data(const DB& db)const { return db.get(dynamic_asset_data_id); } @@ -249,6 +256,72 @@ namespace graphene { namespace chain { > asset_object_multi_index_type; typedef generic_index asset_index; + /** + * @brief contains properties that only apply to dividend-paying assets + * + * @ingroup object + * @ingroup implementation + */ + class asset_dividend_data_object : public abstract_object + { + public: + static const uint8_t space_id = implementation_ids; + static const uint8_t type_id = impl_asset_dividend_data_type; + + /// The tunable options for Dividend-paying assets are stored in this field. + dividend_asset_options options; + + /// The time payouts on this asset were scheduled to be processed last + fc::optional last_scheduled_payout_time; + + /// The time payouts on this asset were last processed + /// (this should be the maintenance interval at or after last_scheduled_payout_time) + fc::optional last_payout_time; + + /// The account which collects pending payouts + account_id_type dividend_distribution_account; + }; + typedef multi_index_container< + asset_dividend_data_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > > + > + > asset_dividend_data_object_multi_index_type; + typedef generic_index asset_dividend_data_object_index; + + + // This tracks the balances in a dividend distribution account at the last time + // pending dividend payouts were calculated (last maintenance interval). + // At each maintenance interval, we will compare the current balance to the + // balance stored here to see how much was deposited during that interval. + class distributed_dividend_balance_object : public abstract_object + { + public: + static const uint8_t space_id = implementation_ids; + static const uint8_t type_id = impl_distributed_dividend_balance_data_type; + + asset_id_type dividend_holder_asset_type; + asset_id_type dividend_payout_asset_type; + share_type balance_at_last_maintenance_interval; + }; + struct by_dividend_payout_asset{}; + typedef multi_index_container< + distributed_dividend_balance_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > >, + ordered_unique< tag, + composite_key< + distributed_dividend_balance_object, + member, + member + > + > + > + > distributed_dividend_balance_object_multi_index_type; + typedef generic_index distributed_dividend_balance_object_index; + + + } } // graphene::chain FC_REFLECT_DERIVED( graphene::chain::asset_dynamic_data_object, (graphene::db::object), @@ -264,6 +337,19 @@ FC_REFLECT_DERIVED( graphene::chain::asset_bitasset_data_object, (graphene::db:: (settlement_price) (settlement_fund) ) + +FC_REFLECT_DERIVED( graphene::chain::asset_dividend_data_object, (graphene::db::object), + (options) + (last_scheduled_payout_time) + (last_payout_time ) + (dividend_distribution_account) + ) + +FC_REFLECT_DERIVED( graphene::chain::distributed_dividend_balance_object, (graphene::db::object), + (dividend_holder_asset_type) + (dividend_payout_asset_type) + (balance_at_last_maintenance_interval) + ) FC_REFLECT_DERIVED( graphene::chain::asset_object, (graphene::db::object), (symbol) diff --git a/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp b/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp index 3f5ede19..eab3ae38 100644 --- a/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp +++ b/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp @@ -112,6 +112,30 @@ namespace graphene { namespace chain { void validate()const; }; + /** + * @brief The dividend_asset_options struct contains configurable options available only to dividend-paying assets. + * + * @note Changes to this struct will break protocol compatibility + */ + struct dividend_asset_options { + /// Time when the next payout should occur. + /// The payouts will happen on the maintenance interval at or after this time + /// If this is set to null, there will be no payouts. + fc::optional next_payout_time; + /// If payouts happen on a fixed schedule, this specifies the interval between + /// payouts in seconds. After each payout, the next payout time will be incremented by + /// this amount. + /// If payout_interval is not set, the next payout (if any) will be the last until + /// the options are updated again. + fc::optional payout_interval; + + extensions_type extensions; + + /// Perform internal consistency checks. + /// @throws fc::exception if any check fails + void validate()const; + }; + /** * @ingroup operations @@ -319,6 +343,35 @@ namespace graphene { namespace chain { void validate()const; }; + /** + * @brief Update options specific to dividend-paying assets + * @ingroup operations + * + * Dividend-paying assets have some options which are not relevant to other asset types. + * This operation is used to update those options an an existing dividend-paying asset. + * This can also be used to convert a non-dividend-paying asset into a dividend-paying + * asset. + * + * @pre @ref issuer MUST be an existing account and MUST match asset_object::issuer on @ref asset_to_update + * @pre @ref fee MUST be nonnegative, and @ref issuer MUST have a sufficient balance to pay it + * @pre @ref new_options SHALL be internally consistent, as verified by @ref validate() + * @post @ref asset_to_update will have dividend-specific options matching those of new_options + */ + struct asset_update_dividend_operation : public base_operation + { + struct fee_parameters_type { uint64_t fee = 500 * GRAPHENE_BLOCKCHAIN_PRECISION; }; + + asset fee; + account_id_type issuer; + asset_id_type asset_to_update; + + dividend_asset_options new_options; + extensions_type extensions; + + account_id_type fee_payer()const { return issuer; } + void validate()const; + }; + /** * @brief Update the set of feed-producing accounts for a BitAsset * @ingroup operations @@ -462,6 +515,13 @@ FC_REFLECT( graphene::chain::asset_options, (description) (extensions) ) + +FC_REFLECT( graphene::chain::dividend_asset_options, + (next_payout_time) + (payout_interval) + (extensions) + ) + FC_REFLECT( graphene::chain::bitasset_options, (feed_lifetime_sec) (minimum_feeds) @@ -480,6 +540,7 @@ FC_REFLECT( graphene::chain::asset_settle_cancel_operation::fee_parameters_type, FC_REFLECT( graphene::chain::asset_fund_fee_pool_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::asset_update_operation::fee_parameters_type, (fee)(price_per_kbyte) ) FC_REFLECT( graphene::chain::asset_update_bitasset_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::asset_update_dividend_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::asset_update_feed_producers_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::asset_publish_feed_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::asset_issue_operation::fee_parameters_type, (fee)(price_per_kbyte) ) @@ -511,6 +572,13 @@ FC_REFLECT( graphene::chain::asset_update_bitasset_operation, (new_options) (extensions) ) +FC_REFLECT( graphene::chain::asset_update_dividend_operation, + (fee) + (issuer) + (asset_to_update) + (new_options) + (extensions) + ) FC_REFLECT( graphene::chain::asset_update_feed_producers_operation, (fee)(issuer)(asset_to_update)(new_feed_producers)(extensions) ) diff --git a/libraries/chain/include/graphene/chain/protocol/operations.hpp b/libraries/chain/include/graphene/chain/protocol/operations.hpp index 097ad854..f9dc0099 100644 --- a/libraries/chain/include/graphene/chain/protocol/operations.hpp +++ b/libraries/chain/include/graphene/chain/protocol/operations.hpp @@ -108,7 +108,8 @@ namespace graphene { namespace chain { betting_market_group_resolved_operation, // VIRTUAL bet_matched_operation, // VIRTUAL bet_cancel_operation, - bet_canceled_operation // VIRTUAL + bet_canceled_operation, // VIRTUAL + asset_update_dividend_operation > operation; /// @} // operations group diff --git a/libraries/chain/include/graphene/chain/protocol/types.hpp b/libraries/chain/include/graphene/chain/protocol/types.hpp index b4d42d47..1d15ea88 100644 --- a/libraries/chain/include/graphene/chain/protocol/types.hpp +++ b/libraries/chain/include/graphene/chain/protocol/types.hpp @@ -164,7 +164,10 @@ namespace graphene { namespace chain { impl_buyback_object_type, impl_fba_accumulator_object_type, impl_betting_market_position_object_type, - impl_global_betting_statistics_object_type + impl_global_betting_statistics_object_type, + impl_asset_dividend_data_type, + impl_pending_dividend_payout_balance_object_type, + impl_distributed_dividend_balance_data_type }; //typedef fc::unsigned_int object_id_type; @@ -232,11 +235,15 @@ namespace graphene { namespace chain { class fba_accumulator_object; class betting_market_position_object; class global_betting_statistics_object; + class asset_dividend_data_object; + class pending_dividend_payout_balance_object; typedef object_id< implementation_ids, impl_global_property_object_type, global_property_object> global_property_id_type; typedef object_id< implementation_ids, impl_dynamic_global_property_object_type, dynamic_global_property_object> dynamic_global_property_id_type; typedef object_id< implementation_ids, impl_asset_dynamic_data_type, asset_dynamic_data_object> asset_dynamic_data_id_type; typedef object_id< implementation_ids, impl_asset_bitasset_data_type, asset_bitasset_data_object> asset_bitasset_data_id_type; + typedef object_id< implementation_ids, impl_asset_dividend_data_type, asset_dividend_data_object> asset_dividend_data_id_type; + typedef object_id< implementation_ids, impl_pending_dividend_payout_balance_object_type, pending_dividend_payout_balance_object> pending_dividend_payout_balance_object_type; typedef object_id< implementation_ids, impl_account_balance_object_type, account_balance_object> account_balance_id_type; typedef object_id< implementation_ids, impl_account_statistics_object_type,account_statistics_object> account_statistics_id_type; typedef object_id< implementation_ids, impl_transaction_object_type, transaction_object> transaction_obj_id_type; @@ -400,6 +407,9 @@ FC_REFLECT_ENUM( graphene::chain::impl_object_type, (impl_fba_accumulator_object_type) (impl_betting_market_position_object_type) (impl_global_betting_statistics_object_type) + (impl_asset_dividend_data_type) + (impl_pending_dividend_payout_balance_object_type) + (impl_distributed_dividend_balance_data_type) ) FC_REFLECT_TYPENAME( graphene::chain::share_type ) diff --git a/libraries/chain/protocol/asset_ops.cpp b/libraries/chain/protocol/asset_ops.cpp index fd1c11be..fdf153a3 100644 --- a/libraries/chain/protocol/asset_ops.cpp +++ b/libraries/chain/protocol/asset_ops.cpp @@ -183,6 +183,12 @@ void asset_update_bitasset_operation::validate() const new_options.validate(); } +void asset_update_dividend_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); + new_options.validate(); +} + void asset_update_feed_producers_operation::validate() const { FC_ASSERT( fee.amount >= 0 ); @@ -201,6 +207,10 @@ void bitasset_options::validate() const FC_ASSERT(maximum_force_settlement_volume <= GRAPHENE_100_PERCENT); } +void dividend_asset_options::validate() const +{ +} + void asset_options::validate()const { FC_ASSERT( max_supply > 0 ); diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index adadf868..c9348ece 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -1199,6 +1199,27 @@ public: return sign_transaction( tx, broadcast ); } FC_CAPTURE_AND_RETHROW( (symbol)(new_options)(broadcast) ) } + signed_transaction update_dividend_asset(string symbol, + dividend_asset_options new_options, + bool broadcast /* = false */) + { try { + optional asset_to_update = find_asset(symbol); + if (!asset_to_update) + FC_THROW("No asset with that symbol exists!"); + + asset_update_dividend_operation update_op; + update_op.issuer = asset_to_update->issuer; + update_op.asset_to_update = asset_to_update->id; + update_op.new_options = new_options; + + signed_transaction tx; + tx.operations.push_back( update_op ); + set_operation_fees( tx, _remote_db->get_global_properties().parameters.current_fees); + tx.validate(); + + return sign_transaction( tx, broadcast ); + } FC_CAPTURE_AND_RETHROW( (symbol)(new_options)(broadcast) ) } + signed_transaction update_asset_feed_producers(string symbol, flat_set new_feed_producers, bool broadcast /* = false */) @@ -2224,7 +2245,7 @@ public: << "\n=====================================================================================" << "|=====================================================================================\n"; - for (int i = 0; i < bids.size() || i < asks.size() ; i++) + for (unsigned i = 0; i < bids.size() || i < asks.size() ; i++) { if ( i < bids.size() ) { @@ -2827,7 +2848,7 @@ vector wallet_api::get_account_history(string name, int limit) auto memo = o.op.visit(detail::operation_printer(ss, *my, o.result)); result.push_back( operation_detail{ memo, ss.str(), o } ); } - if( current.size() < std::min(100,limit) ) + if( (int)current.size() < std::min(100,limit) ) break; limit -= current.size(); } diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index 04fb62ec..b3c11c07 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -1063,6 +1063,24 @@ int64_t database_fixture::get_balance( const account_object& account, const asse return db.get_balance(account.get_id(), a.get_id()).amount.value; } +int64_t database_fixture::get_dividend_pending_payout_balance(asset_id_type dividend_holder_asset_type, + account_id_type dividend_holder_account_id, + asset_id_type dividend_payout_asset_type) const +{ + const pending_dividend_payout_balance_object_index& pending_payout_balance_index = + db.get_index_type(); + dlog("searching ${a}", ("a", dividend_holder_asset_type(db).symbol)); + dlog("searching ${b}", ("b", dividend_payout_asset_type(db).symbol)); + dlog("searching ${c}", ("c", dividend_holder_account_id(db).name)); + dlog("searching ${a} ${b} ${c}", ("a", dividend_holder_asset_type(db).symbol)("b", dividend_payout_asset_type(db).symbol)("c", dividend_holder_account_id(db).name)); + auto pending_payout_iter = + pending_payout_balance_index.indices().get().find(boost::make_tuple(dividend_holder_asset_type, dividend_payout_asset_type, dividend_holder_account_id)); + if (pending_payout_iter == pending_payout_balance_index.indices().get().end()) + return 0; + else + return pending_payout_iter->pending_balance.value; +} + vector< operation_history_object > database_fixture::get_operation_history( account_id_type account_id )const { vector< operation_history_object > result; diff --git a/tests/common/database_fixture.hpp b/tests/common/database_fixture.hpp index de800fe0..92ffb1b3 100644 --- a/tests/common/database_fixture.hpp +++ b/tests/common/database_fixture.hpp @@ -280,6 +280,9 @@ struct database_fixture { void print_joint_market( const string& syma, const string& symb )const; int64_t get_balance( account_id_type account, asset_id_type a )const; int64_t get_balance( const account_object& account, const asset_object& a )const; + int64_t get_dividend_pending_payout_balance(asset_id_type dividend_holder_asset_type, + account_id_type dividend_holder_account_id, + asset_id_type dividend_payout_asset_type) const; vector< operation_history_object > get_operation_history( account_id_type account_id )const; void process_operation_by_witnesses(operation op); const sport_object& create_sport(internationalized_string_type name); diff --git a/tests/tests/operation_tests.cpp b/tests/tests/operation_tests.cpp index 7b3867d7..fc9bdefa 100644 --- a/tests/tests/operation_tests.cpp +++ b/tests/tests/operation_tests.cpp @@ -1111,6 +1111,228 @@ BOOST_AUTO_TEST_CASE( uia_fees ) } } +BOOST_AUTO_TEST_CASE( create_dividend_uia ) +{ + using namespace graphene; + try { + BOOST_TEST_MESSAGE("Creating dividend holder asset"); + { + asset_create_operation creator; + creator.issuer = account_id_type(); + creator.fee = asset(); + creator.symbol = "DIVIDEND"; + creator.common_options.max_supply = 100000000; + creator.precision = 2; + creator.common_options.market_fee_percent = GRAPHENE_MAX_MARKET_FEE_PERCENT/100; /*1%*/ + creator.common_options.issuer_permissions = UIA_ASSET_ISSUER_PERMISSION_MASK; + creator.common_options.flags = charge_market_fee; + creator.common_options.core_exchange_rate = price({asset(2),asset(1,asset_id_type(1))}); + trx.operations.push_back(std::move(creator)); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + } + generate_block(); + + // it should not yet be a divdend asset + const auto& dividend_holder_asset_object = get_asset("DIVIDEND"); + BOOST_CHECK(!dividend_holder_asset_object.dividend_data_id); + + BOOST_TEST_MESSAGE("Converting the new asset to a dividend holder asset"); + { + asset_update_dividend_operation op; + op.issuer = dividend_holder_asset_object.issuer; + op.asset_to_update = dividend_holder_asset_object.id; + op.new_options.next_payout_time = fc::time_point::now() + fc::minutes(1); + op.new_options.payout_interval = 60 * 60 * 24 * 7; // one week + + trx.operations.push_back(op); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + } + generate_block(); + + //const auto& test_readback = get_asset("TEST"); + //BOOST_REQUIRE(test_readback.dividend_data_id); + BOOST_TEST_MESSAGE("Verifying the dividend holder asset options"); + BOOST_REQUIRE(dividend_holder_asset_object.dividend_data_id); + const auto& dividend_data = dividend_holder_asset_object.dividend_data(db); + { + BOOST_REQUIRE(dividend_data.options.payout_interval); + BOOST_CHECK_EQUAL(*dividend_data.options.payout_interval, 60 * 60 * 24 * 7); + } + + BOOST_TEST_MESSAGE("Updating the payout interval"); + { + asset_update_dividend_operation op; + op.issuer = dividend_holder_asset_object.issuer; + op.asset_to_update = dividend_holder_asset_object.id; + op.new_options.next_payout_time = fc::time_point::now() + fc::minutes(1); + op.new_options.payout_interval = 60 * 60 * 24; // one day + trx.operations.push_back(op); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + } + generate_block(); + + BOOST_TEST_MESSAGE("Verifying the updated dividend holder asset options"); + { + BOOST_REQUIRE(dividend_data.options.payout_interval); + BOOST_CHECK_EQUAL(*dividend_data.options.payout_interval, 60 * 60 * 24); + } + + const account_object& dividend_distribution_account = dividend_data.dividend_distribution_account(db); + BOOST_CHECK_EQUAL(dividend_distribution_account.name, "dividend-dividend-distribution"); + + + BOOST_TEST_MESSAGE("Creating test accounts"); + create_account("alice"); + create_account("bob"); + create_account("carol"); + create_account("dave"); + create_account("frank"); + generate_block(); + const account_object& alice = get_account("alice"); + const account_object& bob = get_account("bob"); + const account_object& carol = get_account("carol"); + const account_object& dave = get_account("dave"); + const account_object& frank = get_account("frank"); + + BOOST_TEST_MESSAGE("Creating test asset"); + { + asset_create_operation creator; + creator.issuer = account_id_type(); + creator.fee = asset(); + creator.symbol = "TEST"; + creator.common_options.max_supply = 100000000; + creator.precision = 2; + creator.common_options.market_fee_percent = GRAPHENE_MAX_MARKET_FEE_PERCENT/100; /*1%*/ + creator.common_options.issuer_permissions = UIA_ASSET_ISSUER_PERMISSION_MASK; + creator.common_options.flags = charge_market_fee; + creator.common_options.core_exchange_rate = price({asset(2),asset(1,asset_id_type(1))}); + trx.operations.push_back(std::move(creator)); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + } + generate_block(); + + // it should not yet be a divdend asset + const auto& test_asset_object = get_asset("TEST"); + + auto issue_asset_to_account = [&](const asset_object& asset_to_issue, const account_object& destination_account, int64_t amount_to_issue) + { + asset_issue_operation op; + op.issuer = asset_to_issue.issuer; + op.asset_to_issue = asset(amount_to_issue, asset_to_issue.id); + op.issue_to_account = destination_account.id; + trx.operations.push_back( op ); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + }; + + // Set up the first test, issue alice, bob, and carol each 100 DIVIDEND. + // Then deposit 300 TEST in the distribution account, and see that they + // each are credited 100 TEST. + issue_asset_to_account(dividend_holder_asset_object, alice, 100000); + issue_asset_to_account(dividend_holder_asset_object, bob, 100000); + issue_asset_to_account(dividend_holder_asset_object, carol, 100000); + + BOOST_TEST_MESSAGE("Issuing 300 TEST to the dividend account"); + issue_asset_to_account(test_asset_object, dividend_distribution_account, 30000); + + generate_block(); // get the maintenance skip slots out of the way + + BOOST_TEST_MESSAGE( "Generating blocks until next maintenance interval" ); + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + + // for (const auto& pending_payout : db.get_index_type().indices()) + // dlog("In test, pending payout: ${account_name} -> ${amount}", ("account_name", pending_payout.owner(db).name)("amount", pending_payout.pending_balance)); + + dlog("Test asset object symbol is ${symbol}", ("symbol", test_asset_object.get_id()(db).symbol)); + auto verify_pending_balance = [&](const account_object& holder_account_obj, const asset_object& payout_asset_obj, int64_t expected_balance) { + int64_t pending_balance = get_dividend_pending_payout_balance(dividend_holder_asset_object.id, + holder_account_obj.id, + payout_asset_obj.id); + BOOST_CHECK_EQUAL(pending_balance, expected_balance); + }; + verify_pending_balance(alice, test_asset_object, 10000); + verify_pending_balance(bob, test_asset_object, 10000); + verify_pending_balance(carol, test_asset_object, 10000); + + // For the second test, issue carol more than the other two, so it's + // alice: 100 DIVIDND, bob: 100 DIVIDEND, carol: 200 DIVIDEND + // Then deposit 400 TEST in the distribution account, and see that alice + // and bob are credited with 100 TEST, and carol gets 200 TEST + BOOST_TEST_MESSAGE("Issuing carol twice as much of the holder asset"); + issue_asset_to_account(dividend_holder_asset_object, carol, 100000); // one thousand at two digits of precision + issue_asset_to_account(test_asset_object, dividend_distribution_account, 40000); // one thousand at two digits of precision + BOOST_TEST_MESSAGE( "Generating blocks until next maintenance interval" ); + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + verify_pending_balance(alice, test_asset_object, 20000); + verify_pending_balance(bob, test_asset_object, 20000); + verify_pending_balance(carol, test_asset_object, 30000); + + auto advance_to_next_payout_time = [&]() { + // Advance to the next upcoming payout time + BOOST_REQUIRE(dividend_data.options.next_payout_time); + fc::time_point_sec next_payout_scheduled_time = *dividend_data.options.next_payout_time; + // generate blocks up to the next scheduled time + generate_blocks(next_payout_scheduled_time); + // if the scheduled time fell on a maintenance interval, then we should have paid out. + // if not, we need to advance to the next maintenance interval to trigger the payout + BOOST_REQUIRE(dividend_data.options.next_payout_time); + if (*dividend_data.options.next_payout_time == next_payout_scheduled_time) + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + }; + + fc::time_point_sec old_next_payout_scheduled_time = *dividend_data.options.next_payout_time; + advance_to_next_payout_time(); + + + BOOST_REQUIRE_MESSAGE(dividend_data.options.next_payout_time, "No new payout was scheduled"); + BOOST_CHECK_MESSAGE(old_next_payout_scheduled_time != *dividend_data.options.next_payout_time, + "New payout was scheduled for the same time as the last payout"); + BOOST_CHECK_MESSAGE(old_next_payout_scheduled_time + *dividend_data.options.payout_interval == *dividend_data.options.next_payout_time, + "New payout was not scheduled for the expected time"); + + BOOST_CHECK_EQUAL(get_balance(alice, test_asset_object), 20000); + verify_pending_balance(alice, test_asset_object, 0); + BOOST_CHECK_EQUAL(get_balance(bob, test_asset_object), 20000); + verify_pending_balance(bob, test_asset_object, 0); + BOOST_CHECK_EQUAL(get_balance(carol, test_asset_object), 30000); + verify_pending_balance(carol, test_asset_object, 0); + + + BOOST_TEST_MESSAGE("Removing the payout interval"); + { + asset_update_dividend_operation op; + op.issuer = dividend_holder_asset_object.issuer; + op.asset_to_update = dividend_holder_asset_object.id; + op.new_options.next_payout_time = dividend_data.options.next_payout_time; + op.new_options.payout_interval = fc::optional(); + trx.operations.push_back(op); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + } + generate_block(); + BOOST_CHECK(!dividend_data.options.payout_interval); + advance_to_next_payout_time(); + BOOST_REQUIRE_MESSAGE(!dividend_data.options.next_payout_time, "A new payout was scheduled, but none should have been"); + + } catch(fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + BOOST_AUTO_TEST_CASE( cancel_limit_order_test ) { try { INVOKE( issue_uia ); From b8e1165290d6b06462ffcc8c04dda97a3cf0f8ae Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Mon, 27 Jun 2016 16:24:13 -0400 Subject: [PATCH 03/19] Cherry-picked commit 7857ac4. Correctly generating virtual transactions for payouts --- libraries/app/impacted.cpp | 5 +++ libraries/chain/db_maint.cpp | 37 +++++++++++++++-- .../include/graphene/chain/account_object.hpp | 13 +++++- .../graphene/chain/protocol/asset_ops.hpp | 40 ++++++++++++++++++- .../graphene/chain/protocol/operations.hpp | 3 +- libraries/wallet/wallet.cpp | 20 +++++++++- tests/common/database_fixture.cpp | 8 +--- tests/tests/operation_tests.cpp | 36 ++++++++++++++--- 8 files changed, 141 insertions(+), 21 deletions(-) diff --git a/libraries/app/impacted.cpp b/libraries/app/impacted.cpp index b5dbd979..8dfd3457 100644 --- a/libraries/app/impacted.cpp +++ b/libraries/app/impacted.cpp @@ -91,6 +91,11 @@ struct get_impacted_account_visitor void operator()( const asset_update_bitasset_operation& op ) {} void operator()( const asset_update_dividend_operation& op ) {} + void operator()( const asset_dividend_distribution_operation& op ) + { + _impacted.insert( op.account_id ); + } + void operator()( const asset_update_feed_producers_operation& op ) {} void operator()( const asset_issue_operation& op ) diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index 6bef6051..0f9137b9 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -799,8 +799,8 @@ void schedule_pending_dividend_balances(database& db, dlog("Crediting account ${account} with ${amount}", ("account", holder_balance_object.owner(db).name)("amount", amount_to_credit)); auto pending_payout_iter = - pending_payout_balance_index.indices().get().find(boost::make_tuple(dividend_holder_asset_obj.id, payout_asset_type, holder_balance_object.owner)); - if (pending_payout_iter == pending_payout_balance_index.indices().get().end()) + pending_payout_balance_index.indices().get().find(boost::make_tuple(dividend_holder_asset_obj.id, payout_asset_type, holder_balance_object.owner)); + if (pending_payout_iter == pending_payout_balance_index.indices().get().end()) db.create( [&]( pending_dividend_payout_balance_object& obj ){ obj.owner = holder_balance_object.owner; obj.dividend_holder_asset_type = dividend_holder_asset_obj.id; @@ -839,7 +839,7 @@ void schedule_pending_dividend_balances(database& db, // Reduce all pending payouts proportionally share_type total_pending_balances; auto pending_payouts_range = - pending_payout_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id, payout_asset_type)); + pending_payout_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id, payout_asset_type)); for (const pending_dividend_payout_balance_object& pending_balance_object : boost::make_iterator_range(pending_payouts_range.first, pending_payouts_range.second)) total_pending_balances += pending_balance_object.pending_balance; @@ -927,11 +927,28 @@ void process_dividend_assets(database& db) // for debugging, sum up our payouts here std::map amounts_paid_out_by_asset; #endif + auto pending_payouts_range = - pending_payout_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id)); + pending_payout_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id)); + // the pending_payouts_range is all payouts for this dividend asset, sorted by the holder's account + // we iterate in this order so we can build up a list of payouts for each account to put in the + // virtual op + flat_set payouts_for_this_holder; + fc::optional last_holder_account_id; for (auto pending_balance_object_iter = pending_payouts_range.first; pending_balance_object_iter != pending_payouts_range.second; ) { const pending_dividend_payout_balance_object& pending_balance_object = *pending_balance_object_iter; + + if (last_holder_account_id && *last_holder_account_id != pending_balance_object.owner) + { + // we've moved on to a new account, generate the dividend payment virtual op for the previous one + db.push_applied_operation(asset_dividend_distribution_operation(dividend_holder_asset_obj.id, + *last_holder_account_id, + payouts_for_this_holder)); + ilog("Just pushed virtual op for payout to ${account}", ("account", (*last_holder_account_id)(db).name)); + payouts_for_this_holder.clear(); + } + ilog("Processing payout of ${asset} to account ${account}", ("asset", asset(pending_balance_object.pending_balance, pending_balance_object.dividend_payout_asset_type)) ("account", pending_balance_object.owner(db).name)); @@ -939,6 +956,9 @@ void process_dividend_assets(database& db) db.adjust_balance(pending_balance_object.owner, asset(pending_balance_object.pending_balance, pending_balance_object.dividend_payout_asset_type)); + payouts_for_this_holder.insert(asset(pending_balance_object.pending_balance, + pending_balance_object.dividend_payout_asset_type)); + last_holder_account_id = pending_balance_object.owner; #ifndef NDEBUG amounts_paid_out_by_asset[pending_balance_object.dividend_payout_asset_type] += pending_balance_object.pending_balance; #endif @@ -946,6 +966,15 @@ void process_dividend_assets(database& db) ++pending_balance_object_iter; db.remove(pending_balance_object); } + // we will always be left with the last holder's data, generate the virtual op for it now. + if (last_holder_account_id) + { + // we've moved on to a new account, generate the dividend payment virtual op for the previous one + db.push_applied_operation(asset_dividend_distribution_operation(dividend_holder_asset_obj.id, + *last_holder_account_id, + payouts_for_this_holder)); + ilog("Just pushed virtual op for payout to ${account}", ("account", (*last_holder_account_id)(db).name)); + } // now debit the total amount of dividends paid out from the distribution account auto distributed_balance_range = diff --git a/libraries/chain/include/graphene/chain/account_object.hpp b/libraries/chain/include/graphene/chain/account_object.hpp index d7ba8b34..a90b93ba 100644 --- a/libraries/chain/include/graphene/chain/account_object.hpp +++ b/libraries/chain/include/graphene/chain/account_object.hpp @@ -392,7 +392,8 @@ namespace graphene { namespace chain { */ typedef generic_index account_index; - struct by_dividend_asset_account_asset{}; + struct by_dividend_payout_account{}; + struct by_dividend_account_payout{}; /** * @ingroup object_index @@ -401,13 +402,21 @@ namespace graphene { namespace chain { pending_dividend_payout_balance_object, indexed_by< ordered_unique< tag, member< object, object_id_type, &object::id > >, - ordered_unique< tag, + ordered_unique< tag, composite_key< pending_dividend_payout_balance_object, member, member, member > + >, + ordered_unique< tag, + composite_key< + pending_dividend_payout_balance_object, + member, + member, + member + > > > > pending_dividend_payout_balance_object_multi_index_type; diff --git a/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp b/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp index eab3ae38..db111cbe 100644 --- a/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp +++ b/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp @@ -260,6 +260,43 @@ namespace graphene { namespace chain { { return 0; } }; + /** + * Virtual op generated when a dividend asset pays out dividends + */ + struct asset_dividend_distribution_operation : public base_operation + { + asset_dividend_distribution_operation() {} + asset_dividend_distribution_operation(const asset_id_type& dividend_asset_id, + const account_id_type& account_id, + const flat_set& amounts) : + dividend_asset_id(dividend_asset_id), + account_id(account_id), + amounts(amounts) + {} + struct fee_parameters_type { }; + + asset fee; + + /// The dividend-paying asset which triggered this payout + asset_id_type dividend_asset_id; + + /// The user account receiving the dividends + account_id_type account_id; + + /// The amounts received + flat_set amounts; + + extensions_type extensions; + + account_id_type fee_payer()const { return account_id; } + void validate()const { + FC_ASSERT( false, "virtual operation" ); + } + + share_type calculate_fee(const fee_parameters_type& params)const + { return 0; } + }; + /** * @ingroup operations */ @@ -545,7 +582,7 @@ FC_REFLECT( graphene::chain::asset_update_feed_producers_operation::fee_paramete FC_REFLECT( graphene::chain::asset_publish_feed_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::asset_issue_operation::fee_parameters_type, (fee)(price_per_kbyte) ) FC_REFLECT( graphene::chain::asset_reserve_operation::fee_parameters_type, (fee) ) - +FC_REFLECT( graphene::chain::asset_dividend_distribution_operation::fee_parameters_type, ) FC_REFLECT( graphene::chain::asset_create_operation, (fee) @@ -593,3 +630,4 @@ FC_REFLECT( graphene::chain::asset_reserve_operation, (fee)(payer)(amount_to_reserve)(extensions) ) FC_REFLECT( graphene::chain::asset_fund_fee_pool_operation, (fee)(from_account)(asset_id)(amount)(extensions) ); +FC_REFLECT( graphene::chain::asset_dividend_distribution_operation, (fee)(dividend_asset_id)(account_id)(amounts)(extensions) ); diff --git a/libraries/chain/include/graphene/chain/protocol/operations.hpp b/libraries/chain/include/graphene/chain/protocol/operations.hpp index f9dc0099..5e6fe68d 100644 --- a/libraries/chain/include/graphene/chain/protocol/operations.hpp +++ b/libraries/chain/include/graphene/chain/protocol/operations.hpp @@ -109,7 +109,8 @@ namespace graphene { namespace chain { bet_matched_operation, // VIRTUAL bet_cancel_operation, bet_canceled_operation, // VIRTUAL - asset_update_dividend_operation + asset_update_dividend_operation, + asset_dividend_distribution_operation // VIRTUAL > operation; /// @} // operations group diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index c9348ece..82e555ab 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -123,6 +124,7 @@ public: std::string operator()(const account_create_operation& op)const; std::string operator()(const account_update_operation& op)const; std::string operator()(const asset_create_operation& op)const; + std::string operator()(const asset_dividend_distribution_operation& op)const; }; template @@ -2656,9 +2658,7 @@ std::string operation_printer::operator()(const T& op)const operation_result_printer rprinter(wallet); std::string str_result = result.visit(rprinter); if( str_result != "" ) - { out << " result: " << str_result; - } return ""; } std::string operation_printer::operator()(const transfer_from_blind_operation& op)const @@ -2738,6 +2738,22 @@ std::string operation_printer::operator()(const asset_create_operation& op) cons return fee(op.fee); } +std::string operation_printer::operator()(const asset_dividend_distribution_operation& op)const +{ + asset_object dividend_paying_asset = wallet.get_asset(op.dividend_asset_id); + account_object receiver = wallet.get_account(op.account_id); + + out << receiver.name << " received dividend payments for " << dividend_paying_asset.symbol << ": "; + std::vector pretty_payout_amounts; + for (const asset& payment : op.amounts) + { + asset_object payout_asset = wallet.get_asset(payment.asset); + pretty_payout_amounts.push_back(payout_asset.amount_to_pretty_string(payout_asset.amount)); + } + out << boost::algorithm::join(pretty_payout_amounts, ", "); + return ""; +} + std::string operation_result_printer::operator()(const void_result& x) const { return ""; diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index b3c11c07..726ddb48 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -1069,13 +1069,9 @@ int64_t database_fixture::get_dividend_pending_payout_balance(asset_id_type divi { const pending_dividend_payout_balance_object_index& pending_payout_balance_index = db.get_index_type(); - dlog("searching ${a}", ("a", dividend_holder_asset_type(db).symbol)); - dlog("searching ${b}", ("b", dividend_payout_asset_type(db).symbol)); - dlog("searching ${c}", ("c", dividend_holder_account_id(db).name)); - dlog("searching ${a} ${b} ${c}", ("a", dividend_holder_asset_type(db).symbol)("b", dividend_payout_asset_type(db).symbol)("c", dividend_holder_account_id(db).name)); auto pending_payout_iter = - pending_payout_balance_index.indices().get().find(boost::make_tuple(dividend_holder_asset_type, dividend_payout_asset_type, dividend_holder_account_id)); - if (pending_payout_iter == pending_payout_balance_index.indices().get().end()) + pending_payout_balance_index.indices().get().find(boost::make_tuple(dividend_holder_asset_type, dividend_payout_asset_type, dividend_holder_account_id)); + if (pending_payout_iter == pending_payout_balance_index.indices().get().end()) return 0; else return pending_payout_iter->pending_balance.value; diff --git a/tests/tests/operation_tests.cpp b/tests/tests/operation_tests.cpp index fc9bdefa..cfd47f13 100644 --- a/tests/tests/operation_tests.cpp +++ b/tests/tests/operation_tests.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include @@ -1143,7 +1144,7 @@ BOOST_AUTO_TEST_CASE( create_dividend_uia ) asset_update_dividend_operation op; op.issuer = dividend_holder_asset_object.issuer; op.asset_to_update = dividend_holder_asset_object.id; - op.new_options.next_payout_time = fc::time_point::now() + fc::minutes(1); + op.new_options.next_payout_time = db.head_block_time() + fc::minutes(1); op.new_options.payout_interval = 60 * 60 * 24 * 7; // one week trx.operations.push_back(op); @@ -1286,10 +1287,16 @@ BOOST_AUTO_TEST_CASE( create_dividend_uia ) generate_blocks(next_payout_scheduled_time); // if the scheduled time fell on a maintenance interval, then we should have paid out. // if not, we need to advance to the next maintenance interval to trigger the payout - BOOST_REQUIRE(dividend_data.options.next_payout_time); - if (*dividend_data.options.next_payout_time == next_payout_scheduled_time) - generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); - generate_block(); // get the maintenance skip slots out of the way + if (dividend_data.options.next_payout_time) + { + // we know there was a next_payout_time set when we entered this, so if + // it has been cleared, we must have already processed payouts, no need to + // further advance time. + BOOST_REQUIRE(dividend_data.options.next_payout_time); + if (*dividend_data.options.next_payout_time == next_payout_scheduled_time) + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + } }; fc::time_point_sec old_next_payout_scheduled_time = *dividend_data.options.next_payout_time; @@ -1302,14 +1309,33 @@ BOOST_AUTO_TEST_CASE( create_dividend_uia ) BOOST_CHECK_MESSAGE(old_next_payout_scheduled_time + *dividend_data.options.payout_interval == *dividend_data.options.next_payout_time, "New payout was not scheduled for the expected time"); + auto verify_dividend_payout_operations = [&](const account_object& destination_account, const asset& expected_payout) + { + BOOST_TEST_MESSAGE("Verifying the virtual op was created"); + const account_transaction_history_index& hist_idx = db.get_index_type(); + auto account_history_range = hist_idx.indices().get().equal_range(boost::make_tuple(destination_account.id)); + BOOST_REQUIRE(account_history_range.first != account_history_range.second); + const operation_history_object& history_object = std::prev(account_history_range.second)->operation_id(db); + const asset_dividend_distribution_operation& distribution_operation = history_object.op.get(); + BOOST_CHECK(distribution_operation.account_id == destination_account.id); + BOOST_CHECK(distribution_operation.amounts.find(expected_payout) != distribution_operation.amounts.end()); + }; + + BOOST_TEST_MESSAGE("Verifying the payouts"); BOOST_CHECK_EQUAL(get_balance(alice, test_asset_object), 20000); + verify_dividend_payout_operations(alice, asset(20000, test_asset_object.id)); verify_pending_balance(alice, test_asset_object, 0); + BOOST_CHECK_EQUAL(get_balance(bob, test_asset_object), 20000); + verify_dividend_payout_operations(bob, asset(20000, test_asset_object.id)); verify_pending_balance(bob, test_asset_object, 0); + BOOST_CHECK_EQUAL(get_balance(carol, test_asset_object), 30000); + verify_dividend_payout_operations(carol, asset(30000, test_asset_object.id)); verify_pending_balance(carol, test_asset_object, 0); + BOOST_TEST_MESSAGE("Removing the payout interval"); { asset_update_dividend_operation op; From 60f7dd798fc2c8ade04c1a58e6b78a410f415317 Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Mon, 27 Jun 2016 17:31:14 -0400 Subject: [PATCH 04/19] Keep pending dividend balance and distributed dividend balance objects around (with zero balance) after payouts, they will probably be needed again. --- libraries/chain/db_maint.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index 0f9137b9..d2e44327 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -964,7 +964,9 @@ void process_dividend_assets(database& db) #endif ++pending_balance_object_iter; - db.remove(pending_balance_object); + db.modify(pending_balance_object, [&]( pending_dividend_payout_balance_object& pending_balance ){ + pending_balance.pending_balance = 0; + }); } // we will always be left with the last holder's data, generate the virtual op for it now. if (last_holder_account_id) @@ -1002,7 +1004,9 @@ void process_dividend_assets(database& db) asset(-distributed_balance_object.balance_at_last_maintenance_interval, distributed_balance_object.dividend_payout_asset_type)); ++distributed_balance_object_iter; - db.remove(distributed_balance_object); // now they've been paid out, reset to zero + db.modify(distributed_balance_object, [&]( distributed_dividend_balance_object& obj ){ + obj.balance_at_last_maintenance_interval = 0; // now they've been paid out, reset to zero + }); } // now schedule the next payout time From 5b437d73633a753a65d8c268cf43605d72e4eb2b Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Thu, 30 Jun 2016 12:05:16 -0400 Subject: [PATCH 05/19] Cherry-picked commit b584ee1. Separate out unit tests for dividend-assets into their own test suite --- libraries/app/database_api.cpp | 5 + .../app/include/graphene/app/full_account.hpp | 3 + libraries/chain/db_maint.cpp | 104 +++--- .../include/graphene/chain/account_object.hpp | 13 +- tests/tests/operation_tests.cpp | 327 ++++++++++++------ 5 files changed, 308 insertions(+), 144 deletions(-) diff --git a/libraries/app/database_api.cpp b/libraries/app/database_api.cpp index e36842bf..a1c5f554 100644 --- a/libraries/app/database_api.cpp +++ b/libraries/app/database_api.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -719,6 +720,10 @@ std::map database_api_impl::get_full_accounts( const acnt.withdraws.emplace_back(withdraw); }); + auto pending_payouts_range = + _db.get_index_type().indices().get().equal_range(boost::make_tuple(account->id)); + + std::copy(pending_payouts_range.first, pending_payouts_range.second, std::back_inserter(acnt.pending_dividend_payments)); results[account_name_or_id] = acnt; } diff --git a/libraries/app/include/graphene/app/full_account.hpp b/libraries/app/include/graphene/app/full_account.hpp index dea5eb7e..b30495f9 100644 --- a/libraries/app/include/graphene/app/full_account.hpp +++ b/libraries/app/include/graphene/app/full_account.hpp @@ -48,6 +48,7 @@ namespace graphene { namespace app { vector proposals; vector assets; vector withdraws; + vector pending_dividend_payments; }; } } @@ -68,4 +69,6 @@ FC_REFLECT( graphene::app::full_account, (proposals) (assets) (withdraws) + (proposals) + (pending_dividend_payments) ) diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index d2e44327..93507b7c 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -716,6 +717,10 @@ void deprecate_annual_members( database& db ) return; } +// Schedules payouts from a dividend distribution account to the current holders of the +// dividend-paying asset. This takes any deposits made to the dividend distribution account +// since the last time it was called, and distributes them to the current owners of the +// dividend-paying asset according to the amount they own. void schedule_pending_dividend_balances(database& db, const asset_object& dividend_holder_asset_obj, const asset_dividend_data_object& dividend_data, @@ -723,7 +728,8 @@ void schedule_pending_dividend_balances(database& db, const distributed_dividend_balance_object_index& distributed_dividend_balance_index, const pending_dividend_payout_balance_object_index& pending_payout_balance_index) { - dlog("Processing dividend payments for dividend holder asset asset type ${holder_asset}", ("holder_asset", dividend_holder_asset_obj.symbol)); + dlog("Processing dividend payments for dividend holder asset asset type ${holder_asset} at time ${t}", + ("holder_asset", dividend_holder_asset_obj.symbol)("t", db.head_block_time())); auto current_distribution_account_balance_range = balance_index.indices().get().equal_range(boost::make_tuple(dividend_data.dividend_distribution_account)); auto previous_distribution_account_balance_range = @@ -786,7 +792,8 @@ void schedule_pending_dividend_balances(database& db, share_type remaining_balance_of_dividend_asset = total_balance_of_dividend_asset; for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_account_balance_range.first, holder_account_balance_range.second)) - if (holder_balance_object.owner != dividend_data.dividend_distribution_account) + if (holder_balance_object.owner != dividend_data.dividend_distribution_account && + holder_balance_object.balance.value) { // TODO: if holder_balance_object.owner is able to accept payout_asset_type fc::uint128_t amount_to_credit(remaining_amount_to_distribute.value); @@ -902,6 +909,8 @@ void process_dividend_assets(database& db) if (dividend_holder_asset_obj.dividend_data_id) { const asset_dividend_data_object& dividend_data = dividend_holder_asset_obj.dividend_data(db); + const account_object& dividend_distribution_account_object = dividend_data.dividend_distribution_account(db); + schedule_pending_dividend_balances(db, dividend_holder_asset_obj, dividend_data, balance_index, distributed_dividend_balance_index, pending_payout_balance_index); fc::time_point_sec current_head_block_time = db.head_block_time(); @@ -923,10 +932,7 @@ void process_dividend_assets(database& db) // and use this map to keep track of the total amount of each asset paid out. // Afterwards, we decrease the distribution account's balance by the total amount paid out, // and modify the distributed_balances accordingly -#ifndef NDEBUG - // for debugging, sum up our payouts here std::map amounts_paid_out_by_asset; -#endif auto pending_payouts_range = pending_payout_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id)); @@ -935,6 +941,20 @@ void process_dividend_assets(database& db) // virtual op flat_set payouts_for_this_holder; fc::optional last_holder_account_id; + + // cache the assets the distribution account is approved to send, we will be asking + // for these often + flat_map approved_assets; // assets that the dividend distribution account is authorized to send/receive + auto is_asset_approved_for_distribution_account = [&](const asset_id_type& asset_id) { + auto approved_assets_iter = approved_assets.find(asset_id); + if (approved_assets_iter != approved_assets.end()) + return approved_assets_iter->second; + bool is_approved = is_authorized_asset(db, dividend_distribution_account_object, + asset_id(db)); + approved_assets[asset_id] = is_approved; + return is_approved; + }; + for (auto pending_balance_object_iter = pending_payouts_range.first; pending_balance_object_iter != pending_payouts_range.second; ) { const pending_dividend_payout_balance_object& pending_balance_object = *pending_balance_object_iter; @@ -947,26 +967,31 @@ void process_dividend_assets(database& db) payouts_for_this_holder)); ilog("Just pushed virtual op for payout to ${account}", ("account", (*last_holder_account_id)(db).name)); payouts_for_this_holder.clear(); + last_holder_account_id.reset(); } - ilog("Processing payout of ${asset} to account ${account}", - ("asset", asset(pending_balance_object.pending_balance, pending_balance_object.dividend_payout_asset_type)) - ("account", pending_balance_object.owner(db).name)); - db.adjust_balance(pending_balance_object.owner, - asset(pending_balance_object.pending_balance, - pending_balance_object.dividend_payout_asset_type)); - payouts_for_this_holder.insert(asset(pending_balance_object.pending_balance, - pending_balance_object.dividend_payout_asset_type)); - last_holder_account_id = pending_balance_object.owner; -#ifndef NDEBUG - amounts_paid_out_by_asset[pending_balance_object.dividend_payout_asset_type] += pending_balance_object.pending_balance; -#endif + if (is_authorized_asset(db, pending_balance_object.owner(db), pending_balance_object.dividend_payout_asset_type(db)) && + is_asset_approved_for_distribution_account(pending_balance_object.dividend_payout_asset_type)) + { + ilog("Processing payout of ${asset} to account ${account}", + ("asset", asset(pending_balance_object.pending_balance, pending_balance_object.dividend_payout_asset_type)) + ("account", pending_balance_object.owner(db).name)); + + db.adjust_balance(pending_balance_object.owner, + asset(pending_balance_object.pending_balance, + pending_balance_object.dividend_payout_asset_type)); + payouts_for_this_holder.insert(asset(pending_balance_object.pending_balance, + pending_balance_object.dividend_payout_asset_type)); + last_holder_account_id = pending_balance_object.owner; + amounts_paid_out_by_asset[pending_balance_object.dividend_payout_asset_type] += pending_balance_object.pending_balance; + + db.modify(pending_balance_object, [&]( pending_dividend_payout_balance_object& pending_balance ){ + pending_balance.pending_balance = 0; + }); + } ++pending_balance_object_iter; - db.modify(pending_balance_object, [&]( pending_dividend_payout_balance_object& pending_balance ){ - pending_balance.pending_balance = 0; - }); } // we will always be left with the last holder's data, generate the virtual op for it now. if (last_holder_account_id) @@ -979,34 +1004,25 @@ void process_dividend_assets(database& db) } // now debit the total amount of dividends paid out from the distribution account - auto distributed_balance_range = - distributed_dividend_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id)); + // and reduce the distributed_balances accordingly -#ifndef NDEBUG - // validate that we actually paid out exactly as much as we had planned to - assert(amounts_paid_out_by_asset.size() == std::distance(distributed_balance_range.first, distributed_balance_range.second)); - if (amounts_paid_out_by_asset.size() == std::distance(distributed_balance_range.first, distributed_balance_range.second)) + for (const auto& value : amounts_paid_out_by_asset) { - auto distributed_balance_object_iter = distributed_balance_range.first; - for (const auto& asset_and_amount_paid_out : amounts_paid_out_by_asset) - { - assert(distributed_balance_object_iter->dividend_payout_asset_type == asset_and_amount_paid_out.first); - assert(distributed_balance_object_iter->balance_at_last_maintenance_interval == asset_and_amount_paid_out.second); - ++distributed_balance_object_iter; - } - } -#endif + const asset_id_type& asset_paid_out = value.first; + const share_type& amount_paid_out = value.second; - for (auto distributed_balance_object_iter = distributed_balance_range.first; distributed_balance_object_iter != distributed_balance_range.second; ) - { - const distributed_dividend_balance_object& distributed_balance_object = *distributed_balance_object_iter; db.adjust_balance(dividend_data.dividend_distribution_account, - asset(-distributed_balance_object.balance_at_last_maintenance_interval, - distributed_balance_object.dividend_payout_asset_type)); - ++distributed_balance_object_iter; - db.modify(distributed_balance_object, [&]( distributed_dividend_balance_object& obj ){ - obj.balance_at_last_maintenance_interval = 0; // now they've been paid out, reset to zero - }); + asset(-amount_paid_out, + asset_paid_out)); + auto distributed_balance_iter = + distributed_dividend_balance_index.indices().get().find(boost::make_tuple(dividend_holder_asset_obj.id, + asset_paid_out)); + assert(distributed_balance_iter != distributed_dividend_balance_index.indices().get().end()); + if (distributed_balance_iter != distributed_dividend_balance_index.indices().get().end()) + db.modify(*distributed_balance_iter, [&]( distributed_dividend_balance_object& obj ){ + obj.balance_at_last_maintenance_interval -= amount_paid_out; // now they've been paid out, reset to zero + }); + } // now schedule the next payout time diff --git a/libraries/chain/include/graphene/chain/account_object.hpp b/libraries/chain/include/graphene/chain/account_object.hpp index a90b93ba..4a20cef1 100644 --- a/libraries/chain/include/graphene/chain/account_object.hpp +++ b/libraries/chain/include/graphene/chain/account_object.hpp @@ -392,8 +392,9 @@ namespace graphene { namespace chain { */ typedef generic_index account_index; - struct by_dividend_payout_account{}; - struct by_dividend_account_payout{}; + struct by_dividend_payout_account{}; // use when calculating pending payouts + struct by_dividend_account_payout{}; // use when doing actual payouts + struct by_account_dividend_payout{}; // use in get_full_accounts() /** * @ingroup object_index @@ -417,6 +418,14 @@ namespace graphene { namespace chain { member, member > + >, + ordered_unique< tag, + composite_key< + pending_dividend_payout_balance_object, + member, + member, + member + > > > > pending_dividend_payout_balance_object_multi_index_type; diff --git a/tests/tests/operation_tests.cpp b/tests/tests/operation_tests.cpp index cfd47f13..85ec439f 100644 --- a/tests/tests/operation_tests.cpp +++ b/tests/tests/operation_tests.cpp @@ -1112,6 +1112,8 @@ BOOST_AUTO_TEST_CASE( uia_fees ) } } +BOOST_FIXTURE_TEST_SUITE( dividend_tests, database_fixture ) + BOOST_AUTO_TEST_CASE( create_dividend_uia ) { using namespace graphene; @@ -1133,60 +1135,6 @@ BOOST_AUTO_TEST_CASE( create_dividend_uia ) PUSH_TX( db, trx, ~0 ); trx.operations.clear(); } - generate_block(); - - // it should not yet be a divdend asset - const auto& dividend_holder_asset_object = get_asset("DIVIDEND"); - BOOST_CHECK(!dividend_holder_asset_object.dividend_data_id); - - BOOST_TEST_MESSAGE("Converting the new asset to a dividend holder asset"); - { - asset_update_dividend_operation op; - op.issuer = dividend_holder_asset_object.issuer; - op.asset_to_update = dividend_holder_asset_object.id; - op.new_options.next_payout_time = db.head_block_time() + fc::minutes(1); - op.new_options.payout_interval = 60 * 60 * 24 * 7; // one week - - trx.operations.push_back(op); - set_expiration(db, trx); - PUSH_TX( db, trx, ~0 ); - trx.operations.clear(); - } - generate_block(); - - //const auto& test_readback = get_asset("TEST"); - //BOOST_REQUIRE(test_readback.dividend_data_id); - BOOST_TEST_MESSAGE("Verifying the dividend holder asset options"); - BOOST_REQUIRE(dividend_holder_asset_object.dividend_data_id); - const auto& dividend_data = dividend_holder_asset_object.dividend_data(db); - { - BOOST_REQUIRE(dividend_data.options.payout_interval); - BOOST_CHECK_EQUAL(*dividend_data.options.payout_interval, 60 * 60 * 24 * 7); - } - - BOOST_TEST_MESSAGE("Updating the payout interval"); - { - asset_update_dividend_operation op; - op.issuer = dividend_holder_asset_object.issuer; - op.asset_to_update = dividend_holder_asset_object.id; - op.new_options.next_payout_time = fc::time_point::now() + fc::minutes(1); - op.new_options.payout_interval = 60 * 60 * 24; // one day - trx.operations.push_back(op); - set_expiration(db, trx); - PUSH_TX( db, trx, ~0 ); - trx.operations.clear(); - } - generate_block(); - - BOOST_TEST_MESSAGE("Verifying the updated dividend holder asset options"); - { - BOOST_REQUIRE(dividend_data.options.payout_interval); - BOOST_CHECK_EQUAL(*dividend_data.options.payout_interval, 60 * 60 * 24); - } - - const account_object& dividend_distribution_account = dividend_data.dividend_distribution_account(db); - BOOST_CHECK_EQUAL(dividend_distribution_account.name, "dividend-dividend-distribution"); - BOOST_TEST_MESSAGE("Creating test accounts"); create_account("alice"); @@ -1194,12 +1142,6 @@ BOOST_AUTO_TEST_CASE( create_dividend_uia ) create_account("carol"); create_account("dave"); create_account("frank"); - generate_block(); - const account_object& alice = get_account("alice"); - const account_object& bob = get_account("bob"); - const account_object& carol = get_account("carol"); - const account_object& dave = get_account("dave"); - const account_object& frank = get_account("frank"); BOOST_TEST_MESSAGE("Creating test asset"); { @@ -1220,7 +1162,90 @@ BOOST_AUTO_TEST_CASE( create_dividend_uia ) } generate_block(); - // it should not yet be a divdend asset + // our DIVIDEND asset should not yet be a divdend asset + const auto& dividend_holder_asset_object = get_asset("DIVIDEND"); + BOOST_CHECK(!dividend_holder_asset_object.dividend_data_id); + + BOOST_TEST_MESSAGE("Converting the new asset to a dividend holder asset"); + { + asset_update_dividend_operation op; + op.issuer = dividend_holder_asset_object.issuer; + op.asset_to_update = dividend_holder_asset_object.id; + op.new_options.next_payout_time = db.head_block_time() + fc::minutes(1); + op.new_options.payout_interval = 60 * 60 * 24 * 3; + + trx.operations.push_back(op); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + } + generate_block(); + + BOOST_TEST_MESSAGE("Verifying the dividend holder asset options"); + BOOST_REQUIRE(dividend_holder_asset_object.dividend_data_id); + const auto& dividend_data = dividend_holder_asset_object.dividend_data(db); + { + BOOST_REQUIRE(dividend_data.options.payout_interval); + BOOST_CHECK_EQUAL(*dividend_data.options.payout_interval, 60 * 60 * 24 * 3); + } + + const account_object& dividend_distribution_account = dividend_data.dividend_distribution_account(db); + BOOST_CHECK_EQUAL(dividend_distribution_account.name, "dividend-dividend-distribution"); + + + } catch(fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + +BOOST_AUTO_TEST_CASE( test_update_dividend_interval ) +{ + using namespace graphene; + try { + INVOKE( create_dividend_uia ); + + const auto& dividend_holder_asset_object = get_asset("DIVIDEND"); + const auto& dividend_data = dividend_holder_asset_object.dividend_data(db); + + BOOST_TEST_MESSAGE("Updating the payout interval"); + { + asset_update_dividend_operation op; + op.issuer = dividend_holder_asset_object.issuer; + op.asset_to_update = dividend_holder_asset_object.id; + op.new_options.next_payout_time = fc::time_point::now() + fc::minutes(1); + op.new_options.payout_interval = 60 * 60 * 24; // 1 days + trx.operations.push_back(op); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + } + generate_block(); + + BOOST_TEST_MESSAGE("Verifying the updated dividend holder asset options"); + { + BOOST_REQUIRE(dividend_data.options.payout_interval); + BOOST_CHECK_EQUAL(*dividend_data.options.payout_interval, 60 * 60 * 24); + } + } catch(fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} +BOOST_AUTO_TEST_CASE( test_basic_dividend_distribution ) +{ + using namespace graphene; + try { + INVOKE( create_dividend_uia ); + + const auto& dividend_holder_asset_object = get_asset("DIVIDEND"); + const auto& dividend_data = dividend_holder_asset_object.dividend_data(db); + const account_object& dividend_distribution_account = dividend_data.dividend_distribution_account(db); + const account_object& alice = get_account("alice"); + const account_object& bob = get_account("bob"); + const account_object& carol = get_account("carol"); + const account_object& dave = get_account("dave"); + const account_object& frank = get_account("frank"); const auto& test_asset_object = get_asset("TEST"); auto issue_asset_to_account = [&](const asset_object& asset_to_issue, const account_object& destination_account, int64_t amount_to_issue) @@ -1235,49 +1260,12 @@ BOOST_AUTO_TEST_CASE( create_dividend_uia ) trx.operations.clear(); }; - // Set up the first test, issue alice, bob, and carol each 100 DIVIDEND. - // Then deposit 300 TEST in the distribution account, and see that they - // each are credited 100 TEST. - issue_asset_to_account(dividend_holder_asset_object, alice, 100000); - issue_asset_to_account(dividend_holder_asset_object, bob, 100000); - issue_asset_to_account(dividend_holder_asset_object, carol, 100000); - - BOOST_TEST_MESSAGE("Issuing 300 TEST to the dividend account"); - issue_asset_to_account(test_asset_object, dividend_distribution_account, 30000); - - generate_block(); // get the maintenance skip slots out of the way - - BOOST_TEST_MESSAGE( "Generating blocks until next maintenance interval" ); - generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); - generate_block(); // get the maintenance skip slots out of the way - - // for (const auto& pending_payout : db.get_index_type().indices()) - // dlog("In test, pending payout: ${account_name} -> ${amount}", ("account_name", pending_payout.owner(db).name)("amount", pending_payout.pending_balance)); - - dlog("Test asset object symbol is ${symbol}", ("symbol", test_asset_object.get_id()(db).symbol)); auto verify_pending_balance = [&](const account_object& holder_account_obj, const asset_object& payout_asset_obj, int64_t expected_balance) { int64_t pending_balance = get_dividend_pending_payout_balance(dividend_holder_asset_object.id, holder_account_obj.id, payout_asset_obj.id); BOOST_CHECK_EQUAL(pending_balance, expected_balance); }; - verify_pending_balance(alice, test_asset_object, 10000); - verify_pending_balance(bob, test_asset_object, 10000); - verify_pending_balance(carol, test_asset_object, 10000); - - // For the second test, issue carol more than the other two, so it's - // alice: 100 DIVIDND, bob: 100 DIVIDEND, carol: 200 DIVIDEND - // Then deposit 400 TEST in the distribution account, and see that alice - // and bob are credited with 100 TEST, and carol gets 200 TEST - BOOST_TEST_MESSAGE("Issuing carol twice as much of the holder asset"); - issue_asset_to_account(dividend_holder_asset_object, carol, 100000); // one thousand at two digits of precision - issue_asset_to_account(test_asset_object, dividend_distribution_account, 40000); // one thousand at two digits of precision - BOOST_TEST_MESSAGE( "Generating blocks until next maintenance interval" ); - generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); - generate_block(); // get the maintenance skip slots out of the way - verify_pending_balance(alice, test_asset_object, 20000); - verify_pending_balance(bob, test_asset_object, 20000); - verify_pending_balance(carol, test_asset_object, 30000); auto advance_to_next_payout_time = [&]() { // Advance to the next upcoming payout time @@ -1299,6 +1287,45 @@ BOOST_AUTO_TEST_CASE( create_dividend_uia ) } }; + // the first test will be testing pending balances, so we need to hit a + // maintenance interval that isn't the payout interval. Payout is + // every 3 days, maintenance interval is every 1 day. + advance_to_next_payout_time(); + + // Set up the first test, issue alice, bob, and carol each 100 DIVIDEND. + // Then deposit 300 TEST in the distribution account, and see that they + // each are credited 100 TEST. + issue_asset_to_account(dividend_holder_asset_object, alice, 100000); + issue_asset_to_account(dividend_holder_asset_object, bob, 100000); + issue_asset_to_account(dividend_holder_asset_object, carol, 100000); + + BOOST_TEST_MESSAGE("Issuing 300 TEST to the dividend account"); + issue_asset_to_account(test_asset_object, dividend_distribution_account, 30000); + + generate_block(); + + BOOST_TEST_MESSAGE( "Generating blocks until next maintenance interval" ); + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + + verify_pending_balance(alice, test_asset_object, 10000); + verify_pending_balance(bob, test_asset_object, 10000); + verify_pending_balance(carol, test_asset_object, 10000); + + // For the second test, issue carol more than the other two, so it's + // alice: 100 DIVIDND, bob: 100 DIVIDEND, carol: 200 DIVIDEND + // Then deposit 400 TEST in the distribution account, and see that alice + // and bob are credited with 100 TEST, and carol gets 200 TEST + BOOST_TEST_MESSAGE("Issuing carol twice as much of the holder asset"); + issue_asset_to_account(dividend_holder_asset_object, carol, 100000); // one thousand at two digits of precision + issue_asset_to_account(test_asset_object, dividend_distribution_account, 40000); // one thousand at two digits of precision + BOOST_TEST_MESSAGE( "Generating blocks until next maintenance interval" ); + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + verify_pending_balance(alice, test_asset_object, 20000); + verify_pending_balance(bob, test_asset_object, 20000); + verify_pending_balance(carol, test_asset_object, 30000); + fc::time_point_sec old_next_payout_scheduled_time = *dividend_data.options.next_payout_time; advance_to_next_payout_time(); @@ -1333,8 +1360,111 @@ BOOST_AUTO_TEST_CASE( create_dividend_uia ) BOOST_CHECK_EQUAL(get_balance(carol, test_asset_object), 30000); verify_dividend_payout_operations(carol, asset(30000, test_asset_object.id)); verify_pending_balance(carol, test_asset_object, 0); + } catch(fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} +BOOST_AUTO_TEST_CASE( check_dividend_corner_cases ) +{ + using namespace graphene; + try { + INVOKE( create_dividend_uia ); + const auto& dividend_holder_asset_object = get_asset("DIVIDEND"); + const auto& dividend_data = dividend_holder_asset_object.dividend_data(db); + const account_object& dividend_distribution_account = dividend_data.dividend_distribution_account(db); + const account_object& alice = get_account("alice"); + const account_object& bob = get_account("bob"); + const account_object& carol = get_account("carol"); + const account_object& dave = get_account("dave"); + const account_object& frank = get_account("frank"); + const auto& test_asset_object = get_asset("TEST"); + auto issue_asset_to_account = [&](const asset_object& asset_to_issue, const account_object& destination_account, int64_t amount_to_issue) + { + asset_issue_operation op; + op.issuer = asset_to_issue.issuer; + op.asset_to_issue = asset(amount_to_issue, asset_to_issue.id); + op.issue_to_account = destination_account.id; + trx.operations.push_back( op ); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + }; + + auto verify_pending_balance = [&](const account_object& holder_account_obj, const asset_object& payout_asset_obj, int64_t expected_balance) { + int64_t pending_balance = get_dividend_pending_payout_balance(dividend_holder_asset_object.id, + holder_account_obj.id, + payout_asset_obj.id); + BOOST_CHECK_EQUAL(pending_balance, expected_balance); + }; + + auto reserve_asset_from_account = [&](const asset_object& asset_to_reserve, const account_object& from_account, int64_t amount_to_reserve) + { + asset_reserve_operation reserve_op; + reserve_op.payer = from_account.id; + reserve_op.amount_to_reserve = asset(amount_to_reserve, asset_to_reserve.id); + trx.operations.push_back(reserve_op); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + }; + auto advance_to_next_payout_time = [&]() { + // Advance to the next upcoming payout time + BOOST_REQUIRE(dividend_data.options.next_payout_time); + fc::time_point_sec next_payout_scheduled_time = *dividend_data.options.next_payout_time; + // generate blocks up to the next scheduled time + generate_blocks(next_payout_scheduled_time); + // if the scheduled time fell on a maintenance interval, then we should have paid out. + // if not, we need to advance to the next maintenance interval to trigger the payout + if (dividend_data.options.next_payout_time) + { + // we know there was a next_payout_time set when we entered this, so if + // it has been cleared, we must have already processed payouts, no need to + // further advance time. + BOOST_REQUIRE(dividend_data.options.next_payout_time); + if (*dividend_data.options.next_payout_time == next_payout_scheduled_time) + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + } + }; + + // the first test will be testing pending balances, so we need to hit a + // maintenance interval that isn't the payout interval. Payout is + // every 3 days, maintenance interval is every 1 day. + advance_to_next_payout_time(); + + BOOST_TEST_MESSAGE("Testing a payout interval when there are no users holding the dividend asset"); + BOOST_CHECK_EQUAL(get_balance(bob, dividend_holder_asset_object), 0); + BOOST_CHECK_EQUAL(get_balance(bob, dividend_holder_asset_object), 0); + BOOST_CHECK_EQUAL(get_balance(bob, dividend_holder_asset_object), 0); + issue_asset_to_account(test_asset_object, dividend_distribution_account, 1000); + BOOST_TEST_MESSAGE("Generating blocks until next maintenance interval"); + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + BOOST_TEST_MESSAGE("Verify that no pending payments were scheduled"); + verify_pending_balance(alice, test_asset_object, 0); + verify_pending_balance(bob, test_asset_object, 0); + verify_pending_balance(carol, test_asset_object, 0); + advance_to_next_payout_time(); + BOOST_TEST_MESSAGE("Verify that no actual payments took place"); + verify_pending_balance(alice, test_asset_object, 0); + verify_pending_balance(bob, test_asset_object, 0); + verify_pending_balance(carol, test_asset_object, 0); + BOOST_CHECK_EQUAL(get_balance(alice, test_asset_object), 0); + BOOST_CHECK_EQUAL(get_balance(bob, test_asset_object), 0); + BOOST_CHECK_EQUAL(get_balance(carol, test_asset_object), 0); + BOOST_CHECK_EQUAL(get_balance(dividend_distribution_account, test_asset_object), 1000); + + BOOST_TEST_MESSAGE("Now give alice a small balance and see that she takes it all"); + issue_asset_to_account(dividend_holder_asset_object, alice, 1); + generate_block(); + BOOST_TEST_MESSAGE("Generating blocks until next maintenance interval"); + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + BOOST_TEST_MESSAGE("Verify that no pending payments were scheduled"); + verify_pending_balance(alice, test_asset_object, 1000); BOOST_TEST_MESSAGE("Removing the payout interval"); { @@ -1358,6 +1488,7 @@ BOOST_AUTO_TEST_CASE( create_dividend_uia ) throw; } } +BOOST_AUTO_TEST_SUITE_END() // end dividend_tests suite BOOST_AUTO_TEST_CASE( cancel_limit_order_test ) { try { From f67dd3ea587ad3710dbbecf7f34dfcd5579b395e Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Thu, 30 Jun 2016 16:00:29 -0400 Subject: [PATCH 06/19] Compile fix --- libraries/wallet/wallet.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index 82e555ab..cb5466d6 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -2747,8 +2747,8 @@ std::string operation_printer::operator()(const asset_dividend_distribution_oper std::vector pretty_payout_amounts; for (const asset& payment : op.amounts) { - asset_object payout_asset = wallet.get_asset(payment.asset); - pretty_payout_amounts.push_back(payout_asset.amount_to_pretty_string(payout_asset.amount)); + asset_object payout_asset = wallet.get_asset(payment.asset_id); + pretty_payout_amounts.push_back(payout_asset.amount_to_pretty_string(payment)); } out << boost::algorithm::join(pretty_payout_amounts, ", "); return ""; From 67d08983944cc4aa043cc6c04dbbc13576aa796a Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Thu, 25 Aug 2016 10:41:01 -0400 Subject: [PATCH 07/19] Fixes to paying out non-core assets using their fee pools --- libraries/chain/asset_evaluator.cpp | 5 + libraries/chain/db_maint.cpp | 365 ++++++++++++------ .../include/graphene/chain/asset_object.hpp | 12 + .../graphene/chain/protocol/asset_ops.hpp | 43 ++- .../wallet/include/graphene/wallet/wallet.hpp | 16 + libraries/wallet/wallet.cpp | 8 + tests/tests/operation_tests.cpp | 109 +++++- 7 files changed, 415 insertions(+), 143 deletions(-) diff --git a/libraries/chain/asset_evaluator.cpp b/libraries/chain/asset_evaluator.cpp index 16b6ee18..4e3e9b96 100644 --- a/libraries/chain/asset_evaluator.cpp +++ b/libraries/chain/asset_evaluator.cpp @@ -422,6 +422,11 @@ void_result asset_update_dividend_evaluator::do_apply( const asset_update_divide const asset_dividend_data_object& dividend_data = asset_to_update->dividend_data(d); d.modify(dividend_data, [&]( asset_dividend_data_object& dividend_data_obj ) { dividend_data_obj.options = op.new_options; + // whenever new options are set, clear out the scheduled payout/distribution times + // this will reset and cause the next distribution to happen at the next maintenance + // interval and a payout at the next_payout_time + dividend_data_obj.last_scheduled_payout_time.reset(); + dividend_data_obj.last_scheduled_distribution_time.reset(); }); } return void_result(); diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index 93507b7c..9f418d3e 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -724,11 +724,12 @@ void deprecate_annual_members( database& db ) void schedule_pending_dividend_balances(database& db, const asset_object& dividend_holder_asset_obj, const asset_dividend_data_object& dividend_data, + const fc::time_point_sec& current_head_block_time, const account_balance_index& balance_index, const distributed_dividend_balance_object_index& distributed_dividend_balance_index, const pending_dividend_payout_balance_object_index& pending_payout_balance_index) { - dlog("Processing dividend payments for dividend holder asset asset type ${holder_asset} at time ${t}", + dlog("Processing dividend payments for dividend holder asset type ${holder_asset} at time ${t}", ("holder_asset", dividend_holder_asset_obj.symbol)("t", db.head_block_time())); auto current_distribution_account_balance_range = balance_index.indices().get().equal_range(boost::make_tuple(dividend_data.dividend_distribution_account)); @@ -737,148 +738,257 @@ void schedule_pending_dividend_balances(database& db, // the current range is now all current balances for the distribution account, sorted by asset_type // the previous range is now all previous balances for this account, sorted by asset type + const auto& gpo = db.get_global_properties(); + + // get the list of accounts that hold nonzero balances of the dividend asset + auto holder_balances_begin = + balance_index.indices().get().lower_bound(boost::make_tuple(dividend_holder_asset_obj.id)); + auto holder_balances_end = + balance_index.indices().get().upper_bound(boost::make_tuple(dividend_holder_asset_obj.id, share_type())); + uint32_t holder_account_count = std::distance(holder_balances_begin, holder_balances_end); + uint64_t distribution_base_fee = gpo.parameters.current_fees->get().distribution_base_fee; + uint32_t distribution_fee_per_holder = gpo.parameters.current_fees->get().distribution_fee_per_holder; + // the fee, in BTS, for distributing each asset in the account + uint64_t total_fee_per_asset_in_core = distribution_base_fee + holder_account_count * (uint64_t)distribution_fee_per_holder; + auto current_distribution_account_balance_iter = current_distribution_account_balance_range.first; auto previous_distribution_account_balance_iter = previous_distribution_account_balance_range.first; dlog("Current balances in distribution account: ${current}, Previous balances: ${previous}", ("current", std::distance(current_distribution_account_balance_range.first, current_distribution_account_balance_range.second)) ("previous", std::distance(previous_distribution_account_balance_range.first, previous_distribution_account_balance_range.second))); + // loop through all of the assets currently or previously held in the distribution account while (current_distribution_account_balance_iter != current_distribution_account_balance_range.second || previous_distribution_account_balance_iter != previous_distribution_account_balance_range.second) { - share_type current_balance; - share_type previous_balance; - asset_id_type payout_asset_type; - - if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || - current_distribution_account_balance_iter->asset_type < previous_distribution_account_balance_iter->dividend_payout_asset_type) + try { - payout_asset_type = current_distribution_account_balance_iter->asset_type; - current_balance = current_distribution_account_balance_iter->balance; - idump((payout_asset_type)(current_balance)); - } - else if (current_distribution_account_balance_iter == current_distribution_account_balance_range.second || - previous_distribution_account_balance_iter->dividend_payout_asset_type < current_distribution_account_balance_iter->asset_type) - { - payout_asset_type = previous_distribution_account_balance_iter->dividend_payout_asset_type; - previous_balance = previous_distribution_account_balance_iter->balance_at_last_maintenance_interval; - idump((payout_asset_type)(previous_balance)); - } - else - { - payout_asset_type = current_distribution_account_balance_iter->asset_type; - current_balance = current_distribution_account_balance_iter->balance; - previous_balance = previous_distribution_account_balance_iter->balance_at_last_maintenance_interval; - idump((payout_asset_type)(current_balance)(previous_balance)); - } + // First, figure out how much the balance on this asset has changed since the last sharing out + share_type current_balance; + share_type previous_balance; + asset_id_type payout_asset_type; - share_type delta_balance = current_balance - previous_balance; - dlog("Processing dividend payments of asset type ${payout_asset_type}, delta balance is ${delta_balance}", ("payout_asset_type", payout_asset_type(db).symbol)("delta_balance", delta_balance)); - if (delta_balance > 0) - { - // we need to pay out delta_balance to shareholders proportional to their stake - auto holder_account_balance_range = - balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id)); - share_type total_balance_of_dividend_asset; - for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_account_balance_range.first, holder_account_balance_range.second)) - if (holder_balance_object.owner != dividend_data.dividend_distribution_account) - // TODO: if holder_balance_object.owner is able to accept payout_asset_type - total_balance_of_dividend_asset += holder_balance_object.balance; + if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || + current_distribution_account_balance_iter->asset_type < previous_distribution_account_balance_iter->dividend_payout_asset_type) + { + payout_asset_type = current_distribution_account_balance_iter->asset_type; + current_balance = current_distribution_account_balance_iter->balance; + idump((payout_asset_type)(current_balance)); + } + else if (current_distribution_account_balance_iter == current_distribution_account_balance_range.second || + previous_distribution_account_balance_iter->dividend_payout_asset_type < current_distribution_account_balance_iter->asset_type) + { + payout_asset_type = previous_distribution_account_balance_iter->dividend_payout_asset_type; + previous_balance = previous_distribution_account_balance_iter->balance_at_last_maintenance_interval; + idump((payout_asset_type)(previous_balance)); + } + else + { + payout_asset_type = current_distribution_account_balance_iter->asset_type; + current_balance = current_distribution_account_balance_iter->balance; + previous_balance = previous_distribution_account_balance_iter->balance_at_last_maintenance_interval; + idump((payout_asset_type)(current_balance)(previous_balance)); + } - dlog("There are ${count} holders of the dividend-paying asset, with a total balance of ${total}", - ("count", std::distance(holder_account_balance_range.first, holder_account_balance_range.second)) - ("total", total_balance_of_dividend_asset)); - share_type remaining_amount_to_distribute = delta_balance; - share_type remaining_balance_of_dividend_asset = total_balance_of_dividend_asset; + share_type delta_balance = current_balance - previous_balance; - for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_account_balance_range.first, holder_account_balance_range.second)) - if (holder_balance_object.owner != dividend_data.dividend_distribution_account && - holder_balance_object.balance.value) + // Next, figure out if we want to share this out -- if the amount added to the distribution + // account since last payout is too small, we won't bother. + + share_type total_fee_per_asset_in_payout_asset; + const asset_object* payout_asset_object = nullptr; + if (payout_asset_type == asset_id_type()) + { + payout_asset_object = &db.get_core_asset(); + total_fee_per_asset_in_payout_asset = total_fee_per_asset_in_core; + dlog("Fee for distributing ${payout_asset_type}: ${fee}", + ("payout_asset_type", asset_id_type()(db).symbol) + ("fee", asset(total_fee_per_asset_in_core, asset_id_type()))); + } + else + { + // figure out what the total fee is in terms of the payout asset + const asset_index& asset_object_index = db.get_index_type(); + auto payout_asset_object_iter = asset_object_index.indices().find(payout_asset_type); + FC_ASSERT(payout_asset_object_iter != asset_object_index.indices().end()); + + payout_asset_object = &*payout_asset_object_iter; + asset total_fee_per_asset = asset(total_fee_per_asset_in_core, asset_id_type()) * payout_asset_object->options.core_exchange_rate; + FC_ASSERT(total_fee_per_asset.asset_id == payout_asset_type); + + total_fee_per_asset_in_payout_asset = total_fee_per_asset.amount; + dlog("Fee for distributing ${payout_asset_type}: ${fee}", + ("payout_asset_type", payout_asset_type(db).symbol)("fee", total_fee_per_asset_in_payout_asset)); + } + + share_type minimum_shares_to_distribute; + if (dividend_data.options.minimum_fee_percentage) + { + fc::uint128_t minimum_amount_to_distribute = total_fee_per_asset_in_payout_asset.value; + minimum_amount_to_distribute *= 100 * GRAPHENE_1_PERCENT; + minimum_amount_to_distribute /= dividend_data.options.minimum_fee_percentage; + wdump((total_fee_per_asset_in_payout_asset)(dividend_data.options)); + minimum_shares_to_distribute = minimum_amount_to_distribute.to_uint64(); + } + + dlog("Processing dividend payments of asset type ${payout_asset_type}, delta balance is ${delta_balance}", ("payout_asset_type", payout_asset_type(db).symbol)("delta_balance", delta_balance)); + if (delta_balance > 0) + { + if (delta_balance >= minimum_shares_to_distribute) { - // TODO: if holder_balance_object.owner is able to accept payout_asset_type - fc::uint128_t amount_to_credit(remaining_amount_to_distribute.value); - amount_to_credit *= holder_balance_object.balance.value; - amount_to_credit /= remaining_balance_of_dividend_asset.value; - share_type shares_to_credit((int64_t)amount_to_credit.to_uint64()); + // first, pay the fee for scheduling these dividend payments + if (payout_asset_type == asset_id_type()) + { + // pay fee to network + db.modify(asset_dynamic_data_id_type()(db), [total_fee_per_asset_in_core](asset_dynamic_data_object& d) { + d.accumulated_fees += total_fee_per_asset_in_core; + }); + db.adjust_balance(dividend_data.dividend_distribution_account, + asset(-total_fee_per_asset_in_core, asset_id_type())); + delta_balance -= total_fee_per_asset_in_core; + } + else + { + const asset_dynamic_data_object& dynamic_data = payout_asset_object->dynamic_data(db); + if (dynamic_data.fee_pool < total_fee_per_asset_in_core) + FC_THROW("Not distributing dividends for ${holder_asset_type} in asset ${payout_asset_type} " + "because insufficient funds in fee pool (need: ${need}, have: ${have})", + ("holder_asset_type", dividend_holder_asset_obj.symbol) + ("payout_asset_type", payout_asset_object->symbol) + ("need", asset(total_fee_per_asset_in_core, asset_id_type())) + ("have", asset(dynamic_data.fee_pool, payout_asset_type))); + // deduct the fee from the dividend distribution account + db.adjust_balance(dividend_data.dividend_distribution_account, + asset(-total_fee_per_asset_in_payout_asset, payout_asset_type)); + // convert it to core + db.modify(payout_asset_object->dynamic_data(db), [total_fee_per_asset_in_core, total_fee_per_asset_in_payout_asset](asset_dynamic_data_object& d) { + d.fee_pool -= total_fee_per_asset_in_core; + d.accumulated_fees += total_fee_per_asset_in_payout_asset; + }); + // and pay it to the network + db.modify(asset_dynamic_data_id_type()(db), [total_fee_per_asset_in_core](asset_dynamic_data_object& d) { + d.accumulated_fees += total_fee_per_asset_in_core; + }); + delta_balance -= total_fee_per_asset_in_payout_asset; + } - remaining_amount_to_distribute -= shares_to_credit; - remaining_balance_of_dividend_asset -= holder_balance_object.balance; + // we need to pay out the remaining delta_balance to shareholders proportional to their stake + // so find out what the total stake + share_type total_balance_of_dividend_asset; + for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_balances_begin, holder_balances_end)) + if (holder_balance_object.owner != dividend_data.dividend_distribution_account) + total_balance_of_dividend_asset += holder_balance_object.balance; - dlog("Crediting account ${account} with ${amount}", ("account", holder_balance_object.owner(db).name)("amount", amount_to_credit)); - auto pending_payout_iter = - pending_payout_balance_index.indices().get().find(boost::make_tuple(dividend_holder_asset_obj.id, payout_asset_type, holder_balance_object.owner)); - if (pending_payout_iter == pending_payout_balance_index.indices().get().end()) - db.create( [&]( pending_dividend_payout_balance_object& obj ){ - obj.owner = holder_balance_object.owner; + dlog("There are ${count} holders of the dividend-paying asset, with a total balance of ${total}", + ("count", holder_account_count) + ("total", total_balance_of_dividend_asset)); + share_type remaining_amount_to_distribute = delta_balance; + + // credit each account with their portion + for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_balances_begin, holder_balances_end)) + if (holder_balance_object.owner != dividend_data.dividend_distribution_account && + holder_balance_object.balance.value) + { + fc::uint128_t amount_to_credit(delta_balance.value); + amount_to_credit *= holder_balance_object.balance.value; + amount_to_credit /= total_balance_of_dividend_asset.value; + wdump((delta_balance.value)(holder_balance_object.balance)(total_balance_of_dividend_asset)); + share_type shares_to_credit((int64_t)amount_to_credit.to_uint64()); + + remaining_amount_to_distribute -= shares_to_credit; + + dlog("Crediting account ${account} with ${amount}", + ("account", holder_balance_object.owner(db).name) + ("amount", asset(shares_to_credit, payout_asset_type))); + auto pending_payout_iter = + pending_payout_balance_index.indices().get().find(boost::make_tuple(dividend_holder_asset_obj.id, payout_asset_type, holder_balance_object.owner)); + if (pending_payout_iter == pending_payout_balance_index.indices().get().end()) + db.create( [&]( pending_dividend_payout_balance_object& obj ){ + obj.owner = holder_balance_object.owner; + obj.dividend_holder_asset_type = dividend_holder_asset_obj.id; + obj.dividend_payout_asset_type = payout_asset_type; + obj.pending_balance = shares_to_credit; + }); + else + db.modify(*pending_payout_iter, [&]( pending_dividend_payout_balance_object& pending_balance ){ + pending_balance.pending_balance += shares_to_credit; + }); + } + + for (const auto& pending_payout : pending_payout_balance_index.indices()) + dlog("Pending payout: ${account_name} -> ${amount}", + ("account_name", pending_payout.owner(db).name) + ("amount", asset(pending_payout.pending_balance, pending_payout.dividend_payout_asset_type))); + dlog("Remaining balance not paid out: ${amount}", + ("amount", asset(remaining_amount_to_distribute, payout_asset_type))); + + + share_type distributed_amount = delta_balance - remaining_amount_to_distribute; + if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || + previous_distribution_account_balance_iter->dividend_payout_asset_type != payout_asset_type) + db.create( [&]( distributed_dividend_balance_object& obj ){ obj.dividend_holder_asset_type = dividend_holder_asset_obj.id; obj.dividend_payout_asset_type = payout_asset_type; - obj.pending_balance = shares_to_credit; + obj.balance_at_last_maintenance_interval = distributed_amount; }); else - db.modify(*pending_payout_iter, [&]( pending_dividend_payout_balance_object& pending_balance ){ - pending_balance.pending_balance += shares_to_credit; + db.modify(*previous_distribution_account_balance_iter, [&]( distributed_dividend_balance_object& obj ){ + obj.balance_at_last_maintenance_interval += distributed_amount; }); } - - for (const auto& pending_payout : pending_payout_balance_index.indices()) - { - dlog("Pending payout: ${account_name} -> ${amount}", ("account_name", pending_payout.owner(db).name)("amount", pending_payout.pending_balance)); + else + FC_THROW("Not distributing dividends for ${holder_asset_type} in asset ${payout_asset_type} " + "because amount ${delta_balance} is too small an amount to distribute.", + ("holder_asset_type", dividend_holder_asset_obj.symbol) + ("payout_asset_type", payout_asset_object->symbol) + ("delta_balance", asset(delta_balance, payout_asset_type))); } + else if (delta_balance < 0) + { + // some amount of the asset has been withdrawn from the dividend_distribution_account, + // meaning the current pending payout balances will add up to more than our current balance. + // This should be extremely rare. + // Reduce all pending payouts proportionally + share_type total_pending_balances; + auto pending_payouts_range = + pending_payout_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id, payout_asset_type)); - share_type distributed_amount = current_balance - remaining_amount_to_distribute; - if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || - previous_distribution_account_balance_iter->dividend_payout_asset_type != payout_asset_type) - db.create( [&]( distributed_dividend_balance_object& obj ){ - obj.dividend_holder_asset_type = dividend_holder_asset_obj.id; - obj.dividend_payout_asset_type = payout_asset_type; - obj.balance_at_last_maintenance_interval = distributed_amount; - }); - else - db.modify(*previous_distribution_account_balance_iter, [&]( distributed_dividend_balance_object& obj ){ - obj.balance_at_last_maintenance_interval = distributed_amount; - }); + for (const pending_dividend_payout_balance_object& pending_balance_object : boost::make_iterator_range(pending_payouts_range.first, pending_payouts_range.second)) + total_pending_balances += pending_balance_object.pending_balance; + + share_type remaining_amount_to_recover = -delta_balance; + share_type remaining_pending_balances = total_pending_balances; + for (const pending_dividend_payout_balance_object& pending_balance_object : boost::make_iterator_range(pending_payouts_range.first, pending_payouts_range.second)) + { + fc::uint128_t amount_to_debit(remaining_amount_to_recover.value); + amount_to_debit *= pending_balance_object.pending_balance.value; + amount_to_debit /= remaining_pending_balances.value; + share_type shares_to_debit((int64_t)amount_to_debit.to_uint64()); + + remaining_amount_to_recover -= shares_to_debit; + remaining_pending_balances -= pending_balance_object.pending_balance; + + db.modify(pending_balance_object, [&]( pending_dividend_payout_balance_object& pending_balance ){ + pending_balance.pending_balance -= shares_to_debit; + }); + } + + if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || + previous_distribution_account_balance_iter->dividend_payout_asset_type != payout_asset_type) + db.create( [&]( distributed_dividend_balance_object& obj ){ + obj.dividend_holder_asset_type = dividend_holder_asset_obj.id; + obj.dividend_payout_asset_type = payout_asset_type; + obj.balance_at_last_maintenance_interval = 0; + }); + else + db.modify(*previous_distribution_account_balance_iter, [&]( distributed_dividend_balance_object& obj ){ + obj.balance_at_last_maintenance_interval = 0; + }); + } // end if deposit was large enough to distribute } - else if (delta_balance < 0) + catch (const fc::exception& e) { - // some amount of the asset has been withdrawn from the dividend_distribution_account, - // meaning the current pending payout balances will add up to more than our current balance. - // This should be extremely rare. - // Reduce all pending payouts proportionally - share_type total_pending_balances; - auto pending_payouts_range = - pending_payout_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id, payout_asset_type)); - - for (const pending_dividend_payout_balance_object& pending_balance_object : boost::make_iterator_range(pending_payouts_range.first, pending_payouts_range.second)) - total_pending_balances += pending_balance_object.pending_balance; - - share_type remaining_amount_to_recover = -delta_balance; - share_type remaining_pending_balances = total_pending_balances; - for (const pending_dividend_payout_balance_object& pending_balance_object : boost::make_iterator_range(pending_payouts_range.first, pending_payouts_range.second)) - { - fc::uint128_t amount_to_debit(remaining_amount_to_recover.value); - amount_to_debit *= pending_balance_object.pending_balance.value; - amount_to_debit /= remaining_pending_balances.value; - share_type shares_to_debit((int64_t)amount_to_debit.to_uint64()); - - remaining_amount_to_recover -= shares_to_debit; - remaining_pending_balances -= pending_balance_object.pending_balance; - - db.modify(pending_balance_object, [&]( pending_dividend_payout_balance_object& pending_balance ){ - pending_balance.pending_balance -= shares_to_debit; - }); - } - - if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || - previous_distribution_account_balance_iter->dividend_payout_asset_type != payout_asset_type) - db.create( [&]( distributed_dividend_balance_object& obj ){ - obj.dividend_holder_asset_type = dividend_holder_asset_obj.id; - obj.dividend_payout_asset_type = payout_asset_type; - obj.balance_at_last_maintenance_interval = 0; - }); - else - db.modify(*previous_distribution_account_balance_iter, [&]( distributed_dividend_balance_object& obj ){ - obj.balance_at_last_maintenance_interval = 0; - }); + dlog("${e}", ("e", e)); } // iterate @@ -894,6 +1004,11 @@ void schedule_pending_dividend_balances(database& db, ++previous_distribution_account_balance_iter; } } + db.modify(dividend_data, [current_head_block_time](asset_dividend_data_object& dividend_data_obj) { + dividend_data_obj.last_scheduled_distribution_time = current_head_block_time; + dividend_data_obj.last_distribution_time = current_head_block_time; + }); + } void process_dividend_assets(database& db) @@ -911,9 +1026,10 @@ void process_dividend_assets(database& db) const asset_dividend_data_object& dividend_data = dividend_holder_asset_obj.dividend_data(db); const account_object& dividend_distribution_account_object = dividend_data.dividend_distribution_account(db); - schedule_pending_dividend_balances(db, dividend_holder_asset_obj, dividend_data, - balance_index, distributed_dividend_balance_index, pending_payout_balance_index); fc::time_point_sec current_head_block_time = db.head_block_time(); + + schedule_pending_dividend_balances(db, dividend_holder_asset_obj, dividend_data, current_head_block_time, + balance_index, distributed_dividend_balance_index, pending_payout_balance_index); if (dividend_data.options.next_payout_time && db.head_block_time() >= *dividend_data.options.next_payout_time) { @@ -965,7 +1081,7 @@ void process_dividend_assets(database& db) db.push_applied_operation(asset_dividend_distribution_operation(dividend_holder_asset_obj.id, *last_holder_account_id, payouts_for_this_holder)); - ilog("Just pushed virtual op for payout to ${account}", ("account", (*last_holder_account_id)(db).name)); + dlog("Just pushed virtual op for payout to ${account}", ("account", (*last_holder_account_id)(db).name)); payouts_for_this_holder.clear(); last_holder_account_id.reset(); } @@ -974,7 +1090,7 @@ void process_dividend_assets(database& db) if (is_authorized_asset(db, pending_balance_object.owner(db), pending_balance_object.dividend_payout_asset_type(db)) && is_asset_approved_for_distribution_account(pending_balance_object.dividend_payout_asset_type)) { - ilog("Processing payout of ${asset} to account ${account}", + dlog("Processing payout of ${asset} to account ${account}", ("asset", asset(pending_balance_object.pending_balance, pending_balance_object.dividend_payout_asset_type)) ("account", pending_balance_object.owner(db).name)); @@ -1000,7 +1116,7 @@ void process_dividend_assets(database& db) db.push_applied_operation(asset_dividend_distribution_operation(dividend_holder_asset_obj.id, *last_holder_account_id, payouts_for_this_holder)); - ilog("Just pushed virtual op for payout to ${account}", ("account", (*last_holder_account_id)(db).name)); + dlog("Just pushed virtual op for payout to ${account}", ("account", (*last_holder_account_id)(db).name)); } // now debit the total amount of dividends paid out from the distribution account @@ -1027,7 +1143,6 @@ void process_dividend_assets(database& db) // now schedule the next payout time db.modify(dividend_data, [current_head_block_time](asset_dividend_data_object& dividend_data_obj) { - dlog("Updating dividend payout time, new values are:"); dividend_data_obj.last_scheduled_payout_time = dividend_data_obj.options.next_payout_time; dividend_data_obj.last_payout_time = current_head_block_time; fc::optional next_payout_time; @@ -1044,7 +1159,9 @@ void process_dividend_assets(database& db) next_payout_time = next_possible_time_sec; } dividend_data_obj.options.next_payout_time = next_payout_time; - idump((dividend_data_obj.last_scheduled_payout_time)(dividend_data_obj.last_payout_time)(dividend_data_obj.options.next_payout_time)); + idump((dividend_data_obj.last_scheduled_payout_time) + (dividend_data_obj.last_payout_time) + (dividend_data_obj.options.next_payout_time)); }); } } diff --git a/libraries/chain/include/graphene/chain/asset_object.hpp b/libraries/chain/include/graphene/chain/asset_object.hpp index f41afbe1..62161af7 100644 --- a/libraries/chain/include/graphene/chain/asset_object.hpp +++ b/libraries/chain/include/graphene/chain/asset_object.hpp @@ -272,12 +272,24 @@ namespace graphene { namespace chain { dividend_asset_options options; /// The time payouts on this asset were scheduled to be processed last + /// This field is reset any time the dividend_asset_options are updated fc::optional last_scheduled_payout_time; /// The time payouts on this asset were last processed /// (this should be the maintenance interval at or after last_scheduled_payout_time) + /// This can be displayed for the user fc::optional last_payout_time; + /// The time pending payouts on this asset were last computed, used for + /// correctly computing the next pending payout time. + /// This field is reset any time the dividend_asset_options are updated + fc::optional last_scheduled_distribution_time; + + /// The time pending payouts on this asset were last computed. + /// (this should be the maintenance interval at or after last_scheduled_distribution_time) + /// This can be displayed for the user + fc::optional last_distribution_time; + /// The account which collects pending payouts account_id_type dividend_distribution_account; }; diff --git a/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp b/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp index db111cbe..ca82526c 100644 --- a/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp +++ b/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp @@ -128,6 +128,29 @@ namespace graphene { namespace chain { /// If payout_interval is not set, the next payout (if any) will be the last until /// the options are updated again. fc::optional payout_interval; + /// Each dividend distribution incurs a fee that is based on the number of accounts + /// that hold the dividend asset, not as a percentage of the amount paid out. + /// This parameter prevents assets from being distributed unless the fee is less than + /// the percentage here, to prevent a slow trickle of deposits to the account from being + /// completely consumed. + /// In other words, if you set this parameter to 10% and the fees work out to 100 BTS + /// to share out, balances in the dividend distribution accounts will not be shared out + /// if the balance is less than 10000 BTS. + uint64_t minimum_fee_percentage; + + /// Normally, pending dividend payments are calculated each maintenance interval in + /// which there are balances in the dividend distribution account. At present, this + /// is once per hour on the BitShares blockchain. If this is too often (too expensive + /// in fees or to computationally-intensive for the blockchain) this can be increased. + /// If you set this to, for example, one day, distributions will take place on even + /// multiples of one day, allowing deposits to the distribution account to accumulate + /// for 23 maintenance intervals and then computing the pending payouts on the 24th. + /// + /// Payouts will always occur at the next payout time whether or not it falls on a + /// multiple of the distribution interval, and the timer on the distribution interval + /// are reset at payout time. So if you have the distribution interval at three days + /// and the payout interval at one week, payouts will occur at days 3, 6, 7, 10, 13, 14... + fc::optional minimum_distribution_interval; extensions_type extensions; @@ -273,7 +296,23 @@ namespace graphene { namespace chain { account_id(account_id), amounts(amounts) {} - struct fee_parameters_type { }; + struct fee_parameters_type { + /* note: this is a virtual op and there are no fees directly charged for it */ + + /* Whenever the system computes the pending dividend payments for an asset, + * it charges the distribution_base_fee + distribution_fee_per_holder. + * The computational cost of distributing the dividend payment is proportional + * to the number of dividend holders the asset is divided up among. + */ + /** This fee is charged whenever the system schedules pending dividend + * payments. + */ + uint64_t distribution_base_fee; + /** This fee is charged (in addition to the distribution_base_fee) for each + * user the dividend payment is shared out amongst + */ + uint32_t distribution_fee_per_holder; + }; asset fee; @@ -582,7 +621,7 @@ FC_REFLECT( graphene::chain::asset_update_feed_producers_operation::fee_paramete FC_REFLECT( graphene::chain::asset_publish_feed_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::asset_issue_operation::fee_parameters_type, (fee)(price_per_kbyte) ) FC_REFLECT( graphene::chain::asset_reserve_operation::fee_parameters_type, (fee) ) -FC_REFLECT( graphene::chain::asset_dividend_distribution_operation::fee_parameters_type, ) +FC_REFLECT( graphene::chain::asset_dividend_distribution_operation::fee_parameters_type, (distribution_base_fee)(distribution_fee_per_holder)) FC_REFLECT( graphene::chain::asset_create_operation, (fee) diff --git a/libraries/wallet/include/graphene/wallet/wallet.hpp b/libraries/wallet/include/graphene/wallet/wallet.hpp index 84b17dea..fa48272d 100644 --- a/libraries/wallet/include/graphene/wallet/wallet.hpp +++ b/libraries/wallet/include/graphene/wallet/wallet.hpp @@ -1053,6 +1053,21 @@ class wallet_api bitasset_options new_options, bool broadcast = false); + + /** Update the given asset's dividend asset options. + * + * If the asset is not already a dividend-paying asset, it will be converted into one. + * + * @param symbol the name or id of the asset to update, which must be a market-issued asset + * @param new_options the new dividend_asset_options object, which will entirely replace the existing + * options. + * @param broadcast true to broadcast the transaction on the network + * @returns the signed transaction updating the asset + */ + signed_transaction update_dividend_asset(string symbol, + dividend_asset_options new_options, + bool broadcast = false); + /** Update the set of feed-producing accounts for a BitAsset. * * BitAssets have price feeds selected by taking the median values of recommendations from a set of feed producers. @@ -1697,6 +1712,7 @@ FC_API( graphene::wallet::wallet_api, (create_asset) (update_asset) (update_bitasset) + (update_dividend_asset) (update_asset_feed_producers) (publish_asset_feed) (issue_asset) diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index cb5466d6..136e6f2a 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -3269,6 +3269,14 @@ signed_transaction wallet_api::update_bitasset(string symbol, return my->update_bitasset(symbol, new_options, broadcast); } +signed_transaction wallet_api::update_dividend_asset(string symbol, + dividend_asset_options new_options, + bool broadcast /* = false */) +{ + return my->update_dividend_asset(symbol, new_options, broadcast); +} + + signed_transaction wallet_api::update_asset_feed_producers(string symbol, flat_set new_feed_producers, bool broadcast /* = false */) diff --git a/tests/tests/operation_tests.cpp b/tests/tests/operation_tests.cpp index 85ec439f..d02ee4c0 100644 --- a/tests/tests/operation_tests.cpp +++ b/tests/tests/operation_tests.cpp @@ -1162,6 +1162,18 @@ BOOST_AUTO_TEST_CASE( create_dividend_uia ) } generate_block(); + BOOST_TEST_MESSAGE("Funding asset fee pool"); + { + asset_fund_fee_pool_operation fund_op; + fund_op.from_account = account_id_type(); + fund_op.asset_id = get_asset("TEST").id; + fund_op.amount = 500000000; + trx.operations.push_back(std::move(fund_op)); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + } + // our DIVIDEND asset should not yet be a divdend asset const auto& dividend_holder_asset_object = get_asset("DIVIDEND"); BOOST_CHECK(!dividend_holder_asset_object.dividend_data_id); @@ -1192,6 +1204,12 @@ BOOST_AUTO_TEST_CASE( create_dividend_uia ) const account_object& dividend_distribution_account = dividend_data.dividend_distribution_account(db); BOOST_CHECK_EQUAL(dividend_distribution_account.name, "dividend-dividend-distribution"); + // db.modify( db.get_global_properties(), [&]( global_property_object& _gpo ) + // { + // _gpo.parameters.current_fees->get().distribution_base_fee = 100; + // _gpo.parameters.current_fees->get().distribution_fee_per_holder = 100; + // } ); + } catch(fc::exception& e) { edump((e.to_detail_string())); @@ -1208,6 +1226,26 @@ BOOST_AUTO_TEST_CASE( test_update_dividend_interval ) const auto& dividend_holder_asset_object = get_asset("DIVIDEND"); const auto& dividend_data = dividend_holder_asset_object.dividend_data(db); + auto advance_to_next_payout_time = [&]() { + // Advance to the next upcoming payout time + BOOST_REQUIRE(dividend_data.options.next_payout_time); + fc::time_point_sec next_payout_scheduled_time = *dividend_data.options.next_payout_time; + // generate blocks up to the next scheduled time + generate_blocks(next_payout_scheduled_time); + // if the scheduled time fell on a maintenance interval, then we should have paid out. + // if not, we need to advance to the next maintenance interval to trigger the payout + if (dividend_data.options.next_payout_time) + { + // we know there was a next_payout_time set when we entered this, so if + // it has been cleared, we must have already processed payouts, no need to + // further advance time. + BOOST_REQUIRE(dividend_data.options.next_payout_time); + if (*dividend_data.options.next_payout_time == next_payout_scheduled_time) + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + } + }; + BOOST_TEST_MESSAGE("Updating the payout interval"); { asset_update_dividend_operation op; @@ -1227,6 +1265,23 @@ BOOST_AUTO_TEST_CASE( test_update_dividend_interval ) BOOST_REQUIRE(dividend_data.options.payout_interval); BOOST_CHECK_EQUAL(*dividend_data.options.payout_interval, 60 * 60 * 24); } + + BOOST_TEST_MESSAGE("Removing the payout interval"); + { + asset_update_dividend_operation op; + op.issuer = dividend_holder_asset_object.issuer; + op.asset_to_update = dividend_holder_asset_object.id; + op.new_options.next_payout_time = dividend_data.options.next_payout_time; + op.new_options.payout_interval = fc::optional(); + trx.operations.push_back(op); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + } + generate_block(); + BOOST_CHECK(!dividend_data.options.payout_interval); + advance_to_next_payout_time(); + BOOST_REQUIRE_MESSAGE(!dividend_data.options.next_payout_time, "A new payout was scheduled, but none should have been"); } catch(fc::exception& e) { edump((e.to_detail_string())); throw; @@ -1365,6 +1420,28 @@ BOOST_AUTO_TEST_CASE( test_basic_dividend_distribution ) throw; } } +BOOST_AUTO_TEST_CASE( test_dividend_distribution_interval ) +{ + using namespace graphene; + try { + INVOKE( create_dividend_uia ); + + const auto& dividend_holder_asset_object = get_asset("DIVIDEND"); + const auto& dividend_data = dividend_holder_asset_object.dividend_data(db); + const account_object& dividend_distribution_account = dividend_data.dividend_distribution_account(db); + const account_object& alice = get_account("alice"); + const account_object& bob = get_account("bob"); + const account_object& carol = get_account("carol"); + const account_object& dave = get_account("dave"); + const account_object& frank = get_account("frank"); + const auto& test_asset_object = get_asset("TEST"); + } catch(fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + + BOOST_AUTO_TEST_CASE( check_dividend_corner_cases ) { using namespace graphene; @@ -1463,26 +1540,24 @@ BOOST_AUTO_TEST_CASE( check_dividend_corner_cases ) BOOST_TEST_MESSAGE("Generating blocks until next maintenance interval"); generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); generate_block(); // get the maintenance skip slots out of the way - BOOST_TEST_MESSAGE("Verify that no pending payments were scheduled"); + BOOST_TEST_MESSAGE("Verify that no alice received her payment of the entire amount"); verify_pending_balance(alice, test_asset_object, 1000); - BOOST_TEST_MESSAGE("Removing the payout interval"); - { - asset_update_dividend_operation op; - op.issuer = dividend_holder_asset_object.issuer; - op.asset_to_update = dividend_holder_asset_object.id; - op.new_options.next_payout_time = dividend_data.options.next_payout_time; - op.new_options.payout_interval = fc::optional(); - trx.operations.push_back(op); - set_expiration(db, trx); - PUSH_TX( db, trx, ~0 ); - trx.operations.clear(); - } + // Test that we can pay out the dividend asset itself + issue_asset_to_account(dividend_holder_asset_object, bob, 1); + issue_asset_to_account(dividend_holder_asset_object, carol, 1); + issue_asset_to_account(dividend_holder_asset_object, dividend_distribution_account, 300); generate_block(); - BOOST_CHECK(!dividend_data.options.payout_interval); - advance_to_next_payout_time(); - BOOST_REQUIRE_MESSAGE(!dividend_data.options.next_payout_time, "A new payout was scheduled, but none should have been"); - + BOOST_CHECK_EQUAL(get_balance(alice, dividend_holder_asset_object), 1); + BOOST_CHECK_EQUAL(get_balance(bob, dividend_holder_asset_object), 1); + BOOST_CHECK_EQUAL(get_balance(carol, dividend_holder_asset_object), 1); + BOOST_TEST_MESSAGE("Generating blocks until next maintenance interval"); + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + BOOST_TEST_MESSAGE("Verify that the dividend asset was shared out"); + verify_pending_balance(alice, dividend_holder_asset_object, 100); + verify_pending_balance(bob, dividend_holder_asset_object, 100); + verify_pending_balance(carol, dividend_holder_asset_object, 100); } catch(fc::exception& e) { edump((e.to_detail_string())); throw; From 90722fc0a41a7494bf4080aaa9fae42c9d66e702 Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Fri, 26 Aug 2016 11:29:37 -0400 Subject: [PATCH 08/19] Cherry-picke commit be6ad13. Code cleanups -- renaming variables, adding comments, fix one bug with override transfers and dividend assets --- libraries/app/database_api.cpp | 2 +- .../app/include/graphene/app/full_account.hpp | 3 +- libraries/chain/db_init.cpp | 2 + libraries/chain/db_maint.cpp | 68 +++++++++---------- .../include/graphene/chain/account_object.hpp | 36 +++++----- .../include/graphene/chain/asset_object.hpp | 16 ++--- .../include/graphene/chain/protocol/types.hpp | 8 +-- tests/common/database_fixture.cpp | 4 +- 8 files changed, 71 insertions(+), 68 deletions(-) diff --git a/libraries/app/database_api.cpp b/libraries/app/database_api.cpp index a1c5f554..3bd2699c 100644 --- a/libraries/app/database_api.cpp +++ b/libraries/app/database_api.cpp @@ -721,7 +721,7 @@ std::map database_api_impl::get_full_accounts( const }); auto pending_payouts_range = - _db.get_index_type().indices().get().equal_range(boost::make_tuple(account->id)); + _db.get_index_type().indices().get().equal_range(boost::make_tuple(account->id)); std::copy(pending_payouts_range.first, pending_payouts_range.second, std::back_inserter(acnt.pending_dividend_payments)); diff --git a/libraries/app/include/graphene/app/full_account.hpp b/libraries/app/include/graphene/app/full_account.hpp index b30495f9..955857b7 100644 --- a/libraries/app/include/graphene/app/full_account.hpp +++ b/libraries/app/include/graphene/app/full_account.hpp @@ -48,7 +48,8 @@ namespace graphene { namespace app { vector proposals; vector assets; vector withdraws; - vector pending_dividend_payments; +// vector pending_dividend_payments; + vector pending_dividend_payments; }; } } diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index d19ce204..dcde6dcc 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -276,6 +276,8 @@ void database::initialize_indexes() add_index< primary_index< global_betting_statistics_object_index > >(); add_index< primary_index >(); add_index< primary_index >(); + add_index< primary_index >(); + add_index< primary_index >(); } void database::init_genesis(const genesis_state_type& genesis_state) diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index 9f418d3e..3684c37f 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -726,8 +726,8 @@ void schedule_pending_dividend_balances(database& db, const asset_dividend_data_object& dividend_data, const fc::time_point_sec& current_head_block_time, const account_balance_index& balance_index, - const distributed_dividend_balance_object_index& distributed_dividend_balance_index, - const pending_dividend_payout_balance_object_index& pending_payout_balance_index) + const total_distributed_dividend_balance_object_index& distributed_dividend_balance_index, + const pending_dividend_payout_balance_for_holder_object_index& pending_payout_balance_index) { dlog("Processing dividend payments for dividend holder asset type ${holder_asset} at time ${t}", ("holder_asset", dividend_holder_asset_obj.symbol)("t", db.head_block_time())); @@ -757,6 +757,15 @@ void schedule_pending_dividend_balances(database& db, ("current", std::distance(current_distribution_account_balance_range.first, current_distribution_account_balance_range.second)) ("previous", std::distance(previous_distribution_account_balance_range.first, previous_distribution_account_balance_range.second))); + // when we pay out the dividends to the holders, we need to know the total balance of the dividend asset in all + // accounts other than the distribution account (it would be silly to distribute dividends back to + // the distribution account) + share_type total_balance_of_dividend_asset; + for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_balances_begin, holder_balances_end)) + if (holder_balance_object.owner != dividend_data.dividend_distribution_account) + total_balance_of_dividend_asset += holder_balance_object.balance; + + // loop through all of the assets currently or previously held in the distribution account while (current_distribution_account_balance_iter != current_distribution_account_balance_range.second || previous_distribution_account_balance_iter != previous_distribution_account_balance_range.second) @@ -771,6 +780,7 @@ void schedule_pending_dividend_balances(database& db, if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || current_distribution_account_balance_iter->asset_type < previous_distribution_account_balance_iter->dividend_payout_asset_type) { + // there are no more previous balances or there is no previous balance for this particular asset type payout_asset_type = current_distribution_account_balance_iter->asset_type; current_balance = current_distribution_account_balance_iter->balance; idump((payout_asset_type)(current_balance)); @@ -778,12 +788,14 @@ void schedule_pending_dividend_balances(database& db, else if (current_distribution_account_balance_iter == current_distribution_account_balance_range.second || previous_distribution_account_balance_iter->dividend_payout_asset_type < current_distribution_account_balance_iter->asset_type) { + // there are no more current balances or there is no current balance for this particular previous asset type payout_asset_type = previous_distribution_account_balance_iter->dividend_payout_asset_type; previous_balance = previous_distribution_account_balance_iter->balance_at_last_maintenance_interval; idump((payout_asset_type)(previous_balance)); } else { + // we have both a previous and a current balance for this asset type payout_asset_type = current_distribution_account_balance_iter->asset_type; current_balance = current_distribution_account_balance_iter->balance; previous_balance = previous_distribution_account_balance_iter->balance_at_last_maintenance_interval; @@ -872,19 +884,12 @@ void schedule_pending_dividend_balances(database& db, delta_balance -= total_fee_per_asset_in_payout_asset; } - // we need to pay out the remaining delta_balance to shareholders proportional to their stake - // so find out what the total stake - share_type total_balance_of_dividend_asset; - for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_balances_begin, holder_balances_end)) - if (holder_balance_object.owner != dividend_data.dividend_distribution_account) - total_balance_of_dividend_asset += holder_balance_object.balance; - dlog("There are ${count} holders of the dividend-paying asset, with a total balance of ${total}", ("count", holder_account_count) ("total", total_balance_of_dividend_asset)); share_type remaining_amount_to_distribute = delta_balance; - // credit each account with their portion + // credit each account with their portion, don't send any back to the dividend distribution account for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_balances_begin, holder_balances_end)) if (holder_balance_object.owner != dividend_data.dividend_distribution_account && holder_balance_object.balance.value) @@ -903,14 +908,14 @@ void schedule_pending_dividend_balances(database& db, auto pending_payout_iter = pending_payout_balance_index.indices().get().find(boost::make_tuple(dividend_holder_asset_obj.id, payout_asset_type, holder_balance_object.owner)); if (pending_payout_iter == pending_payout_balance_index.indices().get().end()) - db.create( [&]( pending_dividend_payout_balance_object& obj ){ + db.create( [&]( pending_dividend_payout_balance_for_holder_object& obj ){ obj.owner = holder_balance_object.owner; obj.dividend_holder_asset_type = dividend_holder_asset_obj.id; obj.dividend_payout_asset_type = payout_asset_type; obj.pending_balance = shares_to_credit; }); else - db.modify(*pending_payout_iter, [&]( pending_dividend_payout_balance_object& pending_balance ){ + db.modify(*pending_payout_iter, [&]( pending_dividend_payout_balance_for_holder_object& pending_balance ){ pending_balance.pending_balance += shares_to_credit; }); } @@ -926,13 +931,13 @@ void schedule_pending_dividend_balances(database& db, share_type distributed_amount = delta_balance - remaining_amount_to_distribute; if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || previous_distribution_account_balance_iter->dividend_payout_asset_type != payout_asset_type) - db.create( [&]( distributed_dividend_balance_object& obj ){ + db.create( [&]( total_distributed_dividend_balance_object& obj ){ obj.dividend_holder_asset_type = dividend_holder_asset_obj.id; obj.dividend_payout_asset_type = payout_asset_type; obj.balance_at_last_maintenance_interval = distributed_amount; }); else - db.modify(*previous_distribution_account_balance_iter, [&]( distributed_dividend_balance_object& obj ){ + db.modify(*previous_distribution_account_balance_iter, [&]( total_distributed_dividend_balance_object& obj ){ obj.balance_at_last_maintenance_interval += distributed_amount; }); } @@ -947,18 +952,18 @@ void schedule_pending_dividend_balances(database& db, { // some amount of the asset has been withdrawn from the dividend_distribution_account, // meaning the current pending payout balances will add up to more than our current balance. - // This should be extremely rare. + // This should be extremely rare (caused by an override transfer by the asset owner). // Reduce all pending payouts proportionally share_type total_pending_balances; auto pending_payouts_range = pending_payout_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id, payout_asset_type)); - for (const pending_dividend_payout_balance_object& pending_balance_object : boost::make_iterator_range(pending_payouts_range.first, pending_payouts_range.second)) + for (const pending_dividend_payout_balance_for_holder_object& pending_balance_object : boost::make_iterator_range(pending_payouts_range.first, pending_payouts_range.second)) total_pending_balances += pending_balance_object.pending_balance; share_type remaining_amount_to_recover = -delta_balance; share_type remaining_pending_balances = total_pending_balances; - for (const pending_dividend_payout_balance_object& pending_balance_object : boost::make_iterator_range(pending_payouts_range.first, pending_payouts_range.second)) + for (const pending_dividend_payout_balance_for_holder_object& pending_balance_object : boost::make_iterator_range(pending_payouts_range.first, pending_payouts_range.second)) { fc::uint128_t amount_to_debit(remaining_amount_to_recover.value); amount_to_debit *= pending_balance_object.pending_balance.value; @@ -968,22 +973,17 @@ void schedule_pending_dividend_balances(database& db, remaining_amount_to_recover -= shares_to_debit; remaining_pending_balances -= pending_balance_object.pending_balance; - db.modify(pending_balance_object, [&]( pending_dividend_payout_balance_object& pending_balance ){ + db.modify(pending_balance_object, [&]( pending_dividend_payout_balance_for_holder_object& pending_balance ){ pending_balance.pending_balance -= shares_to_debit; }); } - if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || - previous_distribution_account_balance_iter->dividend_payout_asset_type != payout_asset_type) - db.create( [&]( distributed_dividend_balance_object& obj ){ - obj.dividend_holder_asset_type = dividend_holder_asset_obj.id; - obj.dividend_payout_asset_type = payout_asset_type; - obj.balance_at_last_maintenance_interval = 0; - }); - else - db.modify(*previous_distribution_account_balance_iter, [&]( distributed_dividend_balance_object& obj ){ - obj.balance_at_last_maintenance_interval = 0; - }); + // if we're here, we know there must be a previous balance, so just adjust it by the + // amount we just reclaimed + db.modify(*previous_distribution_account_balance_iter, [&]( total_distributed_dividend_balance_object& obj ){ + obj.balance_at_last_maintenance_interval += delta_balance; + assert(obj.balance_at_last_maintenance_interval == current_balance); + }); } // end if deposit was large enough to distribute } catch (const fc::exception& e) @@ -1016,8 +1016,8 @@ void process_dividend_assets(database& db) ilog("In process_dividend_assets time ${time}", ("time", db.head_block_time())); const account_balance_index& balance_index = db.get_index_type(); - const distributed_dividend_balance_object_index& distributed_dividend_balance_index = db.get_index_type(); - const pending_dividend_payout_balance_object_index& pending_payout_balance_index = db.get_index_type(); + const total_distributed_dividend_balance_object_index& distributed_dividend_balance_index = db.get_index_type(); + const pending_dividend_payout_balance_for_holder_object_index& pending_payout_balance_index = db.get_index_type(); // TODO: switch to iterating over only dividend assets (generalize the by_type index) for( const asset_object& dividend_holder_asset_obj : db.get_index_type().indices() ) @@ -1073,7 +1073,7 @@ void process_dividend_assets(database& db) for (auto pending_balance_object_iter = pending_payouts_range.first; pending_balance_object_iter != pending_payouts_range.second; ) { - const pending_dividend_payout_balance_object& pending_balance_object = *pending_balance_object_iter; + const pending_dividend_payout_balance_for_holder_object& pending_balance_object = *pending_balance_object_iter; if (last_holder_account_id && *last_holder_account_id != pending_balance_object.owner) { @@ -1102,7 +1102,7 @@ void process_dividend_assets(database& db) last_holder_account_id = pending_balance_object.owner; amounts_paid_out_by_asset[pending_balance_object.dividend_payout_asset_type] += pending_balance_object.pending_balance; - db.modify(pending_balance_object, [&]( pending_dividend_payout_balance_object& pending_balance ){ + db.modify(pending_balance_object, [&]( pending_dividend_payout_balance_for_holder_object& pending_balance ){ pending_balance.pending_balance = 0; }); } @@ -1135,7 +1135,7 @@ void process_dividend_assets(database& db) asset_paid_out)); assert(distributed_balance_iter != distributed_dividend_balance_index.indices().get().end()); if (distributed_balance_iter != distributed_dividend_balance_index.indices().get().end()) - db.modify(*distributed_balance_iter, [&]( distributed_dividend_balance_object& obj ){ + db.modify(*distributed_balance_iter, [&]( total_distributed_dividend_balance_object& obj ){ obj.balance_at_last_maintenance_interval -= amount_paid_out; // now they've been paid out, reset to zero }); diff --git a/libraries/chain/include/graphene/chain/account_object.hpp b/libraries/chain/include/graphene/chain/account_object.hpp index 4a20cef1..333168a8 100644 --- a/libraries/chain/include/graphene/chain/account_object.hpp +++ b/libraries/chain/include/graphene/chain/account_object.hpp @@ -321,11 +321,11 @@ namespace graphene { namespace chain { * @ingroup object * */ - class pending_dividend_payout_balance_object : public abstract_object + class pending_dividend_payout_balance_for_holder_object : public abstract_object { public: static const uint8_t space_id = implementation_ids; - static const uint8_t type_id = impl_pending_dividend_payout_balance_object_type; + static const uint8_t type_id = impl_pending_dividend_payout_balance_for_holder_object_type; account_id_type owner; asset_id_type dividend_holder_asset_type; @@ -400,40 +400,40 @@ namespace graphene { namespace chain { * @ingroup object_index */ typedef multi_index_container< - pending_dividend_payout_balance_object, + pending_dividend_payout_balance_for_holder_object, indexed_by< ordered_unique< tag, member< object, object_id_type, &object::id > >, ordered_unique< tag, composite_key< - pending_dividend_payout_balance_object, - member, - member, - member + pending_dividend_payout_balance_for_holder_object, + member, + member, + member > >, ordered_unique< tag, composite_key< - pending_dividend_payout_balance_object, - member, - member, - member + pending_dividend_payout_balance_for_holder_object, + member, + member, + member > >, ordered_unique< tag, composite_key< - pending_dividend_payout_balance_object, - member, - member, - member + pending_dividend_payout_balance_for_holder_object, + member, + member, + member > > > - > pending_dividend_payout_balance_object_multi_index_type; + > pending_dividend_payout_balance_for_holder_object_multi_index_type; /** * @ingroup object_index */ - typedef generic_index pending_dividend_payout_balance_object_index; + typedef generic_index pending_dividend_payout_balance_for_holder_object_index; }} @@ -463,7 +463,7 @@ FC_REFLECT_DERIVED( graphene::chain::account_statistics_object, (pending_fees)(pending_vested_fees) ) -FC_REFLECT_DERIVED( graphene::chain::pending_dividend_payout_balance_object, +FC_REFLECT_DERIVED( graphene::chain::pending_dividend_payout_balance_for_holder_object, (graphene::db::object), (owner)(dividend_holder_asset_type)(dividend_payout_asset_type)(pending_balance) ) diff --git a/libraries/chain/include/graphene/chain/asset_object.hpp b/libraries/chain/include/graphene/chain/asset_object.hpp index 62161af7..3c9d2ee9 100644 --- a/libraries/chain/include/graphene/chain/asset_object.hpp +++ b/libraries/chain/include/graphene/chain/asset_object.hpp @@ -306,7 +306,7 @@ namespace graphene { namespace chain { // pending dividend payouts were calculated (last maintenance interval). // At each maintenance interval, we will compare the current balance to the // balance stored here to see how much was deposited during that interval. - class distributed_dividend_balance_object : public abstract_object + class total_distributed_dividend_balance_object : public abstract_object { public: static const uint8_t space_id = implementation_ids; @@ -318,19 +318,19 @@ namespace graphene { namespace chain { }; struct by_dividend_payout_asset{}; typedef multi_index_container< - distributed_dividend_balance_object, + total_distributed_dividend_balance_object, indexed_by< ordered_unique< tag, member< object, object_id_type, &object::id > >, ordered_unique< tag, composite_key< - distributed_dividend_balance_object, - member, - member + total_distributed_dividend_balance_object, + member, + member > > > - > distributed_dividend_balance_object_multi_index_type; - typedef generic_index distributed_dividend_balance_object_index; + > total_distributed_dividend_balance_object_multi_index_type; + typedef generic_index total_distributed_dividend_balance_object_index; @@ -357,7 +357,7 @@ FC_REFLECT_DERIVED( graphene::chain::asset_dividend_data_object, (graphene::db:: (dividend_distribution_account) ) -FC_REFLECT_DERIVED( graphene::chain::distributed_dividend_balance_object, (graphene::db::object), +FC_REFLECT_DERIVED( graphene::chain::total_distributed_dividend_balance_object, (graphene::db::object), (dividend_holder_asset_type) (dividend_payout_asset_type) (balance_at_last_maintenance_interval) diff --git a/libraries/chain/include/graphene/chain/protocol/types.hpp b/libraries/chain/include/graphene/chain/protocol/types.hpp index 1d15ea88..5b917fbc 100644 --- a/libraries/chain/include/graphene/chain/protocol/types.hpp +++ b/libraries/chain/include/graphene/chain/protocol/types.hpp @@ -166,7 +166,7 @@ namespace graphene { namespace chain { impl_betting_market_position_object_type, impl_global_betting_statistics_object_type, impl_asset_dividend_data_type, - impl_pending_dividend_payout_balance_object_type, + impl_pending_dividend_payout_balance_for_holder_object_type, impl_distributed_dividend_balance_data_type }; @@ -236,14 +236,14 @@ namespace graphene { namespace chain { class betting_market_position_object; class global_betting_statistics_object; class asset_dividend_data_object; - class pending_dividend_payout_balance_object; + class pending_dividend_payout_balance_for_holder_object; typedef object_id< implementation_ids, impl_global_property_object_type, global_property_object> global_property_id_type; typedef object_id< implementation_ids, impl_dynamic_global_property_object_type, dynamic_global_property_object> dynamic_global_property_id_type; typedef object_id< implementation_ids, impl_asset_dynamic_data_type, asset_dynamic_data_object> asset_dynamic_data_id_type; typedef object_id< implementation_ids, impl_asset_bitasset_data_type, asset_bitasset_data_object> asset_bitasset_data_id_type; typedef object_id< implementation_ids, impl_asset_dividend_data_type, asset_dividend_data_object> asset_dividend_data_id_type; - typedef object_id< implementation_ids, impl_pending_dividend_payout_balance_object_type, pending_dividend_payout_balance_object> pending_dividend_payout_balance_object_type; + typedef object_id< implementation_ids, impl_pending_dividend_payout_balance_for_holder_object_type, pending_dividend_payout_balance_for_holder_object> pending_dividend_payout_balance_for_holder_object_type; typedef object_id< implementation_ids, impl_account_balance_object_type, account_balance_object> account_balance_id_type; typedef object_id< implementation_ids, impl_account_statistics_object_type,account_statistics_object> account_statistics_id_type; typedef object_id< implementation_ids, impl_transaction_object_type, transaction_object> transaction_obj_id_type; @@ -408,7 +408,7 @@ FC_REFLECT_ENUM( graphene::chain::impl_object_type, (impl_betting_market_position_object_type) (impl_global_betting_statistics_object_type) (impl_asset_dividend_data_type) - (impl_pending_dividend_payout_balance_object_type) + (impl_pending_dividend_payout_balance_for_holder_object_type) (impl_distributed_dividend_balance_data_type) ) diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index 726ddb48..c187879a 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -1067,8 +1067,8 @@ int64_t database_fixture::get_dividend_pending_payout_balance(asset_id_type divi account_id_type dividend_holder_account_id, asset_id_type dividend_payout_asset_type) const { - const pending_dividend_payout_balance_object_index& pending_payout_balance_index = - db.get_index_type(); + const pending_dividend_payout_balance_for_holder_object_index& pending_payout_balance_index = + db.get_index_type(); auto pending_payout_iter = pending_payout_balance_index.indices().get().find(boost::make_tuple(dividend_holder_asset_type, dividend_payout_asset_type, dividend_holder_account_id)); if (pending_payout_iter == pending_payout_balance_index.indices().get().end()) From da5ef56a357bc8a06b90b6594ae3cbc4dd7a8da4 Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Tue, 30 Aug 2016 14:18:21 -0400 Subject: [PATCH 09/19] Cherry-picked commit 9089292. Prevent creation of accounts with the same name as dividend distribution accounts --- libraries/chain/protocol/account.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libraries/chain/protocol/account.cpp b/libraries/chain/protocol/account.cpp index a63e6f80..3aed8fb3 100644 --- a/libraries/chain/protocol/account.cpp +++ b/libraries/chain/protocol/account.cpp @@ -23,7 +23,7 @@ */ #include #include - +#include namespace graphene { namespace chain { /** @@ -135,6 +135,12 @@ bool is_valid_name( const string& name ) break; begin = end+1; } + + // only dividend distribution accounts linked to a dividend asset can end in -dividend-distribution, and + // these can only be created as a side-effect of the asset_update_dividend_operation + if( boost::algorithm::ends_with(name, "-dividend-distribution") ) + return false; + return true; } FC_CAPTURE_AND_RETHROW( (name) ) } From dac9d8b61b3228acf68db7ea06e3df5ccb1776cc Mon Sep 17 00:00:00 2001 From: Roman Olearski Date: Mon, 7 Nov 2016 10:18:18 +0100 Subject: [PATCH 10/19] Cherry-picked commit 26f4718. Creating default dividend asset --- libraries/chain/db_init.cpp | 54 +++++++++++++++++++ .../include/graphene/chain/asset_object.hpp | 3 ++ .../chain/include/graphene/chain/config.hpp | 2 + .../graphene/chain/protocol/asset_ops.hpp | 2 + 4 files changed, 61 insertions(+) diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index dcde6dcc..d3a58688 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -372,6 +372,16 @@ void database::init_genesis(const genesis_state_type& genesis_state) a.network_fee_percentage = 0; a.lifetime_referrer_fee_percentage = GRAPHENE_100_PERCENT; }).get_id() == GRAPHENE_PROXY_TO_SELF_ACCOUNT); + FC_ASSERT(create([this](account_object& a) { + a.name = "default-dividend-distribution"; + a.statistics = create([&](account_statistics_object& s){s.owner = a.id;}).id; + a.owner.weight_threshold = 1; + a.active.weight_threshold = 1; + a.registrar = a.lifetime_referrer = a.referrer = GRAPHENE_PROXY_TO_SELF_ACCOUNT; + a.membership_expiration_date = time_point_sec::maximum(); + a.network_fee_percentage = 0; + a.lifetime_referrer_fee_percentage = GRAPHENE_100_PERCENT; + }).get_id() == TOURNAMENT_RAKE_FEE_ACCOUNT_ID); // Create more special accounts while( true ) @@ -414,6 +424,46 @@ void database::init_genesis(const genesis_state_type& genesis_state) }); assert( asset_id_type(core_asset.id) == asset().asset_id ); assert( get_balance(account_id_type(), asset_id_type()) == asset(dyn_asset.current_supply) ); + + // Create default dividend asset + const asset_dynamic_data_object& dyn_asset1 = + create([&](asset_dynamic_data_object& a) { + a.current_supply = GRAPHENE_MAX_SHARE_SUPPLY; + }); + const asset_dividend_data_object& div_asset1 = + create([&](asset_dividend_data_object& a) { + a.options.minimum_distribution_interval = 3*24*60*60; + a.options.minimum_fee_percentage = 10*GRAPHENE_1_PERCENT; + a.options.next_payout_time = genesis_state.initial_timestamp + fc::hours(1); + a.options.payout_interval = 7*24*60*60; + a.dividend_distribution_account = TOURNAMENT_RAKE_FEE_ACCOUNT_ID; + }); + const asset_bitasset_data_object& bit_asset1 = + create([&](asset_bitasset_data_object& a) { + a.current_feed.maintenance_collateral_ratio = 1750; + a.current_feed.maximum_short_squeeze_ratio = 1500; + a.current_feed_publication_time = genesis_state.initial_timestamp + fc::hours(1); + }); + + const asset_object& default_asset = + create( [&]( asset_object& a ) { + a.symbol = "DEF"; + a.options.max_market_fee = + a.options.max_supply = genesis_state.max_core_supply; + a.precision = GRAPHENE_BLOCKCHAIN_PRECISION_DIGITS; + a.options.flags = 0; + a.options.issuer_permissions = 79; + a.issuer = TOURNAMENT_RAKE_FEE_ACCOUNT_ID; + a.options.core_exchange_rate.base.amount = 1; + a.options.core_exchange_rate.base.asset_id = asset_id_type(0); + a.options.core_exchange_rate.quote.amount = 1; + a.options.core_exchange_rate.quote.asset_id = asset_id_type(1); + a.dynamic_asset_data_id = dyn_asset1.id; + a.dividend_data_id = div_asset1.id; + a.bitasset_data_id = bit_asset1.id; + }); + assert( default_asset.id == asset_id_type(1) ); + // Create more special assets while( true ) { @@ -503,6 +553,7 @@ void database::init_genesis(const genesis_state_type& genesis_state) const auto& accounts_by_name = get_index_type().indices().get(); auto get_account_id = [&accounts_by_name](const string& name) { auto itr = accounts_by_name.find(name); + if (itr == accounts_by_name.end()) return GRAPHENE_NULL_ACCOUNT; FC_ASSERT(itr != accounts_by_name.end(), "Unable to find account '${acct}'. Did you forget to add a record for it to initial_accounts?", ("acct", name)); @@ -635,6 +686,8 @@ void database::init_genesis(const genesis_state_type& genesis_state) { total_supplies[ asset_id_type(0) ] = GRAPHENE_MAX_SHARE_SUPPLY; } + total_debts[ asset_id_type(1) ] = + total_supplies[ asset_id_type(1) ] = 0; const auto& idx = get_index_type().indices().get(); auto it = idx.begin(); @@ -654,6 +707,7 @@ void database::init_genesis(const genesis_state_type& genesis_state) elog( "Genesis for asset ${aname} is not balanced\n" " Debt is ${debt}\n" " Supply is ${supply}\n", + ("aname", debt_itr->first) ("debt", debt_itr->second) ("supply", supply_itr->second) ); diff --git a/libraries/chain/include/graphene/chain/asset_object.hpp b/libraries/chain/include/graphene/chain/asset_object.hpp index 3c9d2ee9..e33e7300 100644 --- a/libraries/chain/include/graphene/chain/asset_object.hpp +++ b/libraries/chain/include/graphene/chain/asset_object.hpp @@ -354,6 +354,8 @@ FC_REFLECT_DERIVED( graphene::chain::asset_dividend_data_object, (graphene::db:: (options) (last_scheduled_payout_time) (last_payout_time ) + (last_scheduled_distribution_time) + (last_distribution_time) (dividend_distribution_account) ) @@ -371,4 +373,5 @@ FC_REFLECT_DERIVED( graphene::chain::asset_object, (graphene::db::object), (dynamic_asset_data_id) (bitasset_data_id) (buyback_account) + (dividend_data_id) ) diff --git a/libraries/chain/include/graphene/chain/config.hpp b/libraries/chain/include/graphene/chain/config.hpp index 96449867..4cc147ac 100644 --- a/libraries/chain/include/graphene/chain/config.hpp +++ b/libraries/chain/include/graphene/chain/config.hpp @@ -164,6 +164,8 @@ #define GRAPHENE_TEMP_ACCOUNT (graphene::chain::account_id_type(4)) /// Represents the canonical account for specifying you will vote directly (as opposed to a proxy) #define GRAPHENE_PROXY_TO_SELF_ACCOUNT (graphene::chain::account_id_type(5)) +/// +#define TOURNAMENT_RAKE_FEE_ACCOUNT_ID (graphene::chain::account_id_type(6)) /// Sentinel value used in the scheduler. #define GRAPHENE_NULL_WITNESS (graphene::chain::witness_id_type(0)) ///@} diff --git a/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp b/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp index ca82526c..62c9c9a2 100644 --- a/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp +++ b/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp @@ -595,6 +595,8 @@ FC_REFLECT( graphene::chain::asset_options, FC_REFLECT( graphene::chain::dividend_asset_options, (next_payout_time) (payout_interval) + (minimum_fee_percentage) + (minimum_distribution_interval) (extensions) ) From 1386617465816c7e26d855329b00bc0241b74355 Mon Sep 17 00:00:00 2001 From: Roman Olearski Date: Mon, 7 Nov 2016 11:05:04 +0100 Subject: [PATCH 11/19] Removing redundant line --- libraries/chain/db_init.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index d3a58688..37ced012 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -553,7 +553,6 @@ void database::init_genesis(const genesis_state_type& genesis_state) const auto& accounts_by_name = get_index_type().indices().get(); auto get_account_id = [&accounts_by_name](const string& name) { auto itr = accounts_by_name.find(name); - if (itr == accounts_by_name.end()) return GRAPHENE_NULL_ACCOUNT; FC_ASSERT(itr != accounts_by_name.end(), "Unable to find account '${acct}'. Did you forget to add a record for it to initial_accounts?", ("acct", name)); From 85102da5047499ce2fea32623dbd347631531343 Mon Sep 17 00:00:00 2001 From: Roman Olearski Date: Tue, 8 Nov 2016 11:34:04 +0100 Subject: [PATCH 12/19] Cherry-picked commit 32b5343. Implementing propose_dividend_asset_update --- libraries/chain/db_init.cpp | 2 +- .../wallet/include/graphene/wallet/wallet.hpp | 15 ++++++ libraries/wallet/wallet.cpp | 53 +++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index 37ced012..9adbb024 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -415,7 +415,7 @@ void database::init_genesis(const genesis_state_type& genesis_state) a.precision = GRAPHENE_BLOCKCHAIN_PRECISION_DIGITS; a.options.flags = 0; a.options.issuer_permissions = 0; - a.issuer = GRAPHENE_NULL_ACCOUNT; + a.issuer = GRAPHENE_COMMITTEE_ACCOUNT; a.options.core_exchange_rate.base.amount = 1; a.options.core_exchange_rate.base.asset_id = asset_id_type(0); a.options.core_exchange_rate.quote.amount = 1; diff --git a/libraries/wallet/include/graphene/wallet/wallet.hpp b/libraries/wallet/include/graphene/wallet/wallet.hpp index fa48272d..18beea55 100644 --- a/libraries/wallet/include/graphene/wallet/wallet.hpp +++ b/libraries/wallet/include/graphene/wallet/wallet.hpp @@ -1499,6 +1499,20 @@ class wallet_api const variant_object& changed_values, bool broadcast = false); + /** Propose a dividend asset update. + * + * @param proposing_account The account paying the fee to propose the tx + * @param expiration_time Timestamp specifying when the proposal will either take effect or expire. + * @param changed_values dividend asset parameters to update + * @param broadcast true if you wish to broadcast the transaction + * @return the signed version of the transaction + */ + signed_transaction propose_dividend_asset_update( + const string& proposing_account, + fc::time_point_sec expiration_time, + const variant_object& changed_values, + bool broadcast = false); + /** Approve or disapprove a proposal. * * @param fee_paying_account The account paying the fee for the op. @@ -1761,6 +1775,7 @@ FC_API( graphene::wallet::wallet_api, (get_prototype_operation) (propose_parameter_change) (propose_fee_change) + (propose_dividend_asset_update) (approve_proposal) (dbg_make_uia) (dbg_make_mia) diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index 136e6f2a..a8cf2a21 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -2411,6 +2411,46 @@ public: return sign_transaction(tx, broadcast); } + signed_transaction propose_dividend_asset_update( + const string& proposing_account, + fc::time_point_sec expiration_time, + const variant_object& changed_values, + bool broadcast = false) + { + FC_ASSERT( changed_values.contains("asset_to_update") ); + + const chain_parameters& current_params = get_global_properties().parameters; + asset_update_dividend_operation changed_op; + fc::reflector::visit( + fc::from_variant_visitor( changed_values, changed_op ) + ); + + optional asset_to_update = find_asset(changed_op.asset_to_update); + if (!asset_to_update) + FC_THROW("No asset with that symbol exists!"); + + asset_update_dividend_operation update_op; + update_op.issuer = asset_to_update->issuer; + update_op.asset_to_update = asset_to_update->id; + update_op.new_options = changed_op.new_options; + + proposal_create_operation prop_op; + + prop_op.expiration_time = expiration_time; + prop_op.review_period_seconds = current_params.committee_proposal_review_period; + prop_op.fee_paying_account = get_account(proposing_account).id; + + prop_op.proposed_ops.emplace_back( update_op ); + current_params.current_fees->set_fee( prop_op.proposed_ops.back().op ); + + signed_transaction tx; + tx.operations.push_back(prop_op); + set_operation_fees(tx, current_params.current_fees); + tx.validate(); + + return sign_transaction(tx, broadcast); + } + signed_transaction approve_proposal( const string& fee_paying_account, const string& proposal_id, @@ -3524,6 +3564,16 @@ signed_transaction wallet_api::propose_fee_change( return my->propose_fee_change( proposing_account, expiration_time, changed_fees, broadcast ); } +signed_transaction wallet_api::propose_dividend_asset_update( + const string& proposing_account, + fc::time_point_sec expiration_time, + const variant_object& changed_fees, + bool broadcast /* = false */ + ) +{ + return my->propose_dividend_asset_update( proposing_account, expiration_time, changed_fees, broadcast ); +} + signed_transaction wallet_api::approve_proposal( const string& fee_paying_account, const string& proposal_id, @@ -3534,6 +3584,9 @@ signed_transaction wallet_api::approve_proposal( return my->approve_proposal( fee_paying_account, proposal_id, delta, broadcast ); } + + + global_property_object wallet_api::get_global_properties() const { return my->get_global_properties(); From bfcc23ac9dde3fc2034d6e684232904051412145 Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Tue, 15 Nov 2016 10:23:58 -0500 Subject: [PATCH 13/19] Cherry-picked commit 749ffb8. Don't make the core asset a bitasset --- libraries/chain/db_init.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index 9adbb024..9d2f5b7a 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -408,6 +408,16 @@ void database::init_genesis(const genesis_state_type& genesis_state) create([&](asset_dynamic_data_object& a) { a.current_supply = GRAPHENE_MAX_SHARE_SUPPLY; }); + + const asset_dividend_data_object& div_asset = + create([&](asset_dividend_data_object& a) { + a.options.minimum_distribution_interval = 3*24*60*60; + a.options.minimum_fee_percentage = 10*GRAPHENE_1_PERCENT; + a.options.next_payout_time = genesis_state.initial_timestamp + fc::hours(1); + a.options.payout_interval = 7*24*60*60; + a.dividend_distribution_account = TOURNAMENT_RAKE_FEE_ACCOUNT_ID; + }); + const asset_object& core_asset = create( [&]( asset_object& a ) { a.symbol = GRAPHENE_SYMBOL; @@ -421,7 +431,8 @@ void database::init_genesis(const genesis_state_type& genesis_state) a.options.core_exchange_rate.quote.amount = 1; a.options.core_exchange_rate.quote.asset_id = asset_id_type(0); a.dynamic_asset_data_id = dyn_asset.id; - }); + a.dividend_data_id = div_asset.id; + }); assert( asset_id_type(core_asset.id) == asset().asset_id ); assert( get_balance(account_id_type(), asset_id_type()) == asset(dyn_asset.current_supply) ); @@ -438,12 +449,6 @@ void database::init_genesis(const genesis_state_type& genesis_state) a.options.payout_interval = 7*24*60*60; a.dividend_distribution_account = TOURNAMENT_RAKE_FEE_ACCOUNT_ID; }); - const asset_bitasset_data_object& bit_asset1 = - create([&](asset_bitasset_data_object& a) { - a.current_feed.maintenance_collateral_ratio = 1750; - a.current_feed.maximum_short_squeeze_ratio = 1500; - a.current_feed_publication_time = genesis_state.initial_timestamp + fc::hours(1); - }); const asset_object& default_asset = create( [&]( asset_object& a ) { @@ -460,7 +465,6 @@ void database::init_genesis(const genesis_state_type& genesis_state) a.options.core_exchange_rate.quote.asset_id = asset_id_type(1); a.dynamic_asset_data_id = dyn_asset1.id; a.dividend_data_id = div_asset1.id; - a.bitasset_data_id = bit_asset1.id; }); assert( default_asset.id == asset_id_type(1) ); From 64d59602a83d41c20f48422b0111848b4bf737e5 Mon Sep 17 00:00:00 2001 From: Roman Olearski Date: Fri, 25 Nov 2016 11:23:06 +0100 Subject: [PATCH 14/19] solution proposal for ...issues/#7 Allow vesting core tokens to vote and receive dividends --- libraries/chain/db_maint.cpp | 135 ++++++++++++++++-- .../graphene/chain/vesting_balance_object.hpp | 21 ++- 2 files changed, 142 insertions(+), 14 deletions(-) diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index 3684c37f..3c6890b8 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -47,6 +47,8 @@ #include #include +#define USE_VESTING_OBJECT_BY_ASSET_BALANCE_INDEX // vesting_balance_object by_asset_balance index needed + namespace graphene { namespace chain { template @@ -726,14 +728,15 @@ void schedule_pending_dividend_balances(database& db, const asset_dividend_data_object& dividend_data, const fc::time_point_sec& current_head_block_time, const account_balance_index& balance_index, + const vesting_balance_index& vesting_index, const total_distributed_dividend_balance_object_index& distributed_dividend_balance_index, const pending_dividend_payout_balance_for_holder_object_index& pending_payout_balance_index) { - dlog("Processing dividend payments for dividend holder asset type ${holder_asset} at time ${t}", + dlog("Processing dividend payments for dividend holder asset type ${holder_asset} at time ${t}", ("holder_asset", dividend_holder_asset_obj.symbol)("t", db.head_block_time())); auto current_distribution_account_balance_range = balance_index.indices().get().equal_range(boost::make_tuple(dividend_data.dividend_distribution_account)); - auto previous_distribution_account_balance_range = + auto previous_distribution_account_balance_range = distributed_dividend_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id)); // the current range is now all current balances for the distribution account, sorted by asset_type // the previous range is now all previous balances for this account, sorted by asset type @@ -743,7 +746,7 @@ void schedule_pending_dividend_balances(database& db, // get the list of accounts that hold nonzero balances of the dividend asset auto holder_balances_begin = balance_index.indices().get().lower_bound(boost::make_tuple(dividend_holder_asset_obj.id)); - auto holder_balances_end = + auto holder_balances_end = balance_index.indices().get().upper_bound(boost::make_tuple(dividend_holder_asset_obj.id, share_type())); uint32_t holder_account_count = std::distance(holder_balances_begin, holder_balances_end); uint64_t distribution_base_fee = gpo.parameters.current_fees->get().distribution_base_fee; @@ -751,6 +754,35 @@ void schedule_pending_dividend_balances(database& db, // the fee, in BTS, for distributing each asset in the account uint64_t total_fee_per_asset_in_core = distribution_base_fee + holder_account_count * (uint64_t)distribution_fee_per_holder; + std::map vesting_amounts; +#ifdef USE_VESTING_OBJECT_BY_ASSET_BALANCE_INDEX + // get only once a collection of accounts that hold nonzero vesting balances of the dividend asset + auto vesting_balances_begin = + vesting_index.indices().get().lower_bound(boost::make_tuple(dividend_holder_asset_obj.id)); + auto vesting_balances_end = + vesting_index.indices().get().upper_bound(boost::make_tuple(dividend_holder_asset_obj.id, share_type())); + for (const vesting_balance_object& vesting_balance_obj : boost::make_iterator_range(vesting_balances_begin, vesting_balances_end)) + { + vesting_amounts[vesting_balance_obj.owner] += vesting_balance_obj.balance.amount; + dlog("Vesting balance for account: ${owner}, amount: ${amount}", + ("owner", vesting_balance_obj.owner(db).name) + ("amount", vesting_balance_obj.balance.amount)); + } +#else + // get only once a collection of accounts that hold nonzero vesting balances of the dividend asset + const auto& vesting_balances = vesting_index.indices().get(); + for (const vesting_balance_object& vesting_balance_obj : vesting_balances) + { + if (vesting_balance_obj.balance.asset_id == dividend_holder_asset_obj.id && vesting_balance_obj.balance.amount) + { + vesting_amounts[vesting_balance_obj.owner] += vesting_balance_obj.balance.amount; + dlog("Vesting balance for account: ${owner}, amount: ${amount}", + ("owner", vesting_balance_obj.owner(db).name) + ("amount", vesting_balance_obj.balance.amount)); + } + } +#endif + auto current_distribution_account_balance_iter = current_distribution_account_balance_range.first; auto previous_distribution_account_balance_iter = previous_distribution_account_balance_range.first; dlog("Current balances in distribution account: ${current}, Previous balances: ${previous}", @@ -763,9 +795,24 @@ void schedule_pending_dividend_balances(database& db, share_type total_balance_of_dividend_asset; for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_balances_begin, holder_balances_end)) if (holder_balance_object.owner != dividend_data.dividend_distribution_account) + { total_balance_of_dividend_asset += holder_balance_object.balance; - - + auto itr = vesting_amounts.find(holder_balance_object.owner); + if (itr != vesting_amounts.end()) + total_balance_of_dividend_asset += itr->second; +// // working, but potential performance gap? +// auto vesting_range = vesting_index.indices().get().equal_range(holder_balance_object.owner); +// for (const vesting_balance_object& vesting_balance : boost::make_iterator_range(vesting_range.first, vesting_range.second) +// { +// if (vesting_balance.balance.asset_id == dividend_holder_asset_obj.id) +// { +// total_balance_of_dividend_asset += vesting_balance.balance.amount; +// dlog("Vesting balances for account: ${owner}, amount: ${amount}", +// ("owner", vesting_balance.owner(db).name) +// ("amount", vesting_balance.balance.amount)); +// } +// } + } // loop through all of the assets currently or previously held in the distribution account while (current_distribution_account_balance_iter != current_distribution_account_balance_range.second || previous_distribution_account_balance_iter != previous_distribution_account_balance_range.second) @@ -891,13 +938,33 @@ void schedule_pending_dividend_balances(database& db, // credit each account with their portion, don't send any back to the dividend distribution account for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_balances_begin, holder_balances_end)) - if (holder_balance_object.owner != dividend_data.dividend_distribution_account && - holder_balance_object.balance.value) + { + //if (holder_balance_object.owner != dividend_data.dividend_distribution_account && holder_balance_object.balance.value) + if (holder_balance_object.owner == dividend_data.dividend_distribution_account) continue; + + auto holder_balance = holder_balance_object.balance; + + auto itr = vesting_amounts.find(holder_balance_object.owner); + if (itr != vesting_amounts.end()) + holder_balance += itr->second; +// // working, but potential performance gap? +// auto vesting_range = vesting_index.indices().get().equal_range(holder_balance_object.owner); +// for (const vesting_balance_object& vesting_balance : boost::make_iterator_range(vesting_range.first, vesting_range.second)) +// { +// if (vesting_balance.balance.asset_id == dividend_holder_asset_obj.id) +// { +// holder_balance += vesting_balance.balance.amount; +// dlog("Vesting balances for account: ${owner}, amount: ${amount}", +// ("owner", vesting_balance.owner(db).name) +// ("amount", vesting_balance.balance.amount)); +// } +// } + if (holder_balance.value) { - fc::uint128_t amount_to_credit(delta_balance.value); - amount_to_credit *= holder_balance_object.balance.value; + fc::uint128_t amount_to_credit(delta_balance.value); + amount_to_credit *= holder_balance.value; amount_to_credit /= total_balance_of_dividend_asset.value; - wdump((delta_balance.value)(holder_balance_object.balance)(total_balance_of_dividend_asset)); + wdump((delta_balance.value)(holder_balance)(total_balance_of_dividend_asset)); share_type shares_to_credit((int64_t)amount_to_credit.to_uint64()); remaining_amount_to_distribute -= shares_to_credit; @@ -919,6 +986,7 @@ void schedule_pending_dividend_balances(database& db, pending_balance.pending_balance += shares_to_credit; }); } + } for (const auto& pending_payout : pending_payout_balance_index.indices()) dlog("Pending payout: ${account_name} -> ${amount}", @@ -927,7 +995,6 @@ void schedule_pending_dividend_balances(database& db, dlog("Remaining balance not paid out: ${amount}", ("amount", asset(remaining_amount_to_distribute, payout_asset_type))); - share_type distributed_amount = delta_balance - remaining_amount_to_distribute; if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || previous_distribution_account_balance_iter->dividend_payout_asset_type != payout_asset_type) @@ -1016,6 +1083,7 @@ void process_dividend_assets(database& db) ilog("In process_dividend_assets time ${time}", ("time", db.head_block_time())); const account_balance_index& balance_index = db.get_index_type(); + const vesting_balance_index& vbalance_index = db.get_index_type(); const total_distributed_dividend_balance_object_index& distributed_dividend_balance_index = db.get_index_type(); const pending_dividend_payout_balance_for_holder_object_index& pending_payout_balance_index = db.get_index_type(); @@ -1029,7 +1097,7 @@ void process_dividend_assets(database& db) fc::time_point_sec current_head_block_time = db.head_block_time(); schedule_pending_dividend_balances(db, dividend_holder_asset_obj, dividend_data, current_head_block_time, - balance_index, distributed_dividend_balance_index, pending_payout_balance_index); + balance_index, vbalance_index, distributed_dividend_balance_index, pending_payout_balance_index); if (dividend_data.options.next_payout_time && db.head_block_time() >= *dividend_data.options.next_payout_time) { @@ -1179,6 +1247,7 @@ void database::perform_chain_maintenance(const signed_block& next_block, const g struct vote_tally_helper { database& d; const global_property_object& props; + std::map vesting_amounts; vote_tally_helper(database& d, const global_property_object& gpo) : d(d), props(gpo) @@ -1187,6 +1256,33 @@ void database::perform_chain_maintenance(const signed_block& next_block, const g d._witness_count_histogram_buffer.resize(props.parameters.maximum_witness_count / 2 + 1); d._committee_count_histogram_buffer.resize(props.parameters.maximum_committee_count / 2 + 1); d._total_voting_stake = 0; + + const vesting_balance_index& vesting_index = d.get_index_type(); +#ifdef USE_VESTING_OBJECT_BY_ASSET_BALANCE_INDEX + auto vesting_balances_begin = + vesting_index.indices().get().lower_bound(boost::make_tuple(asset_id_type())); + auto vesting_balances_end = + vesting_index.indices().get().upper_bound(boost::make_tuple(asset_id_type(), share_type())); + for (const vesting_balance_object& vesting_balance_obj : boost::make_iterator_range(vesting_balances_begin, vesting_balances_end)) + { + vesting_amounts[vesting_balance_obj.owner] += vesting_balance_obj.balance.amount; + dlog("Vesting balance for account: ${owner}, amount: ${amount}", + ("owner", vesting_balance_obj.owner(d).name) + ("amount", vesting_balance_obj.balance.amount)); + } +#else + const auto& vesting_balances = vesting_index.indices().get(); + for (const vesting_balance_object& vesting_balance_obj : vesting_balances) + { + if (vesting_balance_obj.balance.asset_id == asset_id_type() && vesting_balance_obj.balance.amount) + { + vesting_amounts[vesting_balance_obj.owner] += vesting_balance_obj.balance.amount; + dlog("Vesting balance for account: ${owner}, amount: ${amount}", + ("owner", vesting_balance_obj.owner(d).name) + ("amount", vesting_balance_obj.balance.amount)); + } + } +#endif } void operator()(const account_object& stake_account) { @@ -1205,6 +1301,21 @@ void database::perform_chain_maintenance(const signed_block& next_block, const g + (stake_account.cashback_vb.valid() ? (*stake_account.cashback_vb)(d).balance.amount.value: 0) + d.get_balance(stake_account.get_id(), asset_id_type()).amount.value; + auto itr = vesting_amounts.find(stake_account.id); + if (itr != vesting_amounts.end()) + voting_stake += itr->second.value; +// // working, but potential performance gap? +// auto vesting_range = d.get_index_type().indices().get().equal_range(stake_account.id); +// for (const vesting_balance_object& vesting_balance : boost::make_iterator_range(vesting_range.first, vesting_range.second)) +// { +// if (vesting_balance.balance.asset_id == asset_id_type()) +// { +// voting_stake += vesting_balance.balance.amount.value; +// dlog("Vote_tally_helper vesting balances for account: ${owner}, amount: ${amount}", +// ("owner", vesting_balance.owner(d).name) +// ("amount", vesting_balance.balance.amount)); +// } +// } for( vote_id_type id : opinion_account.options.votes ) { uint32_t offset = id.instance(); diff --git a/libraries/chain/include/graphene/chain/vesting_balance_object.hpp b/libraries/chain/include/graphene/chain/vesting_balance_object.hpp index 210c6c58..49b3464c 100644 --- a/libraries/chain/include/graphene/chain/vesting_balance_object.hpp +++ b/libraries/chain/include/graphene/chain/vesting_balance_object.hpp @@ -31,8 +31,10 @@ #include #include +#include - +#define offset_d(i,f) (long(&(i)->f) - long(i)) +#define offset_s(t,f) offset_d((t*)1000, f) namespace graphene { namespace chain { using namespace graphene::db; @@ -171,13 +173,28 @@ namespace graphene { namespace chain { * @ingroup object_index */ struct by_account; + struct by_asset_balance; typedef multi_index_container< vesting_balance_object, indexed_by< ordered_unique< tag, member< object, object_id_type, &object::id > >, ordered_non_unique< tag, member - > + >, + ordered_unique< tag, + composite_key< + vesting_balance_object, + member_offset, + member_offset + //member + //member_offset + >, + composite_key_compare< + std::less< asset_id_type >, + std::greater< share_type > + //std::less< account_id_type > + > + > > > vesting_balance_multi_index_type; /** From 9bbf73a96a59a02ed663fbfec299c53b94ac1996 Mon Sep 17 00:00:00 2001 From: Roman Olearski Date: Fri, 25 Nov 2016 19:40:25 +0100 Subject: [PATCH 15/19] Cherry-picked commit 81c9e98. solution ...issues/#7 Allow vesting core tokens to vote and receive dividends --- libraries/chain/db_maint.cpp | 37 ------------------------------------ 1 file changed, 37 deletions(-) diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index 3c6890b8..d83b68e0 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -800,18 +800,6 @@ void schedule_pending_dividend_balances(database& db, auto itr = vesting_amounts.find(holder_balance_object.owner); if (itr != vesting_amounts.end()) total_balance_of_dividend_asset += itr->second; -// // working, but potential performance gap? -// auto vesting_range = vesting_index.indices().get().equal_range(holder_balance_object.owner); -// for (const vesting_balance_object& vesting_balance : boost::make_iterator_range(vesting_range.first, vesting_range.second) -// { -// if (vesting_balance.balance.asset_id == dividend_holder_asset_obj.id) -// { -// total_balance_of_dividend_asset += vesting_balance.balance.amount; -// dlog("Vesting balances for account: ${owner}, amount: ${amount}", -// ("owner", vesting_balance.owner(db).name) -// ("amount", vesting_balance.balance.amount)); -// } -// } } // loop through all of the assets currently or previously held in the distribution account while (current_distribution_account_balance_iter != current_distribution_account_balance_range.second || @@ -939,7 +927,6 @@ void schedule_pending_dividend_balances(database& db, // credit each account with their portion, don't send any back to the dividend distribution account for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_balances_begin, holder_balances_end)) { - //if (holder_balance_object.owner != dividend_data.dividend_distribution_account && holder_balance_object.balance.value) if (holder_balance_object.owner == dividend_data.dividend_distribution_account) continue; auto holder_balance = holder_balance_object.balance; @@ -947,18 +934,6 @@ void schedule_pending_dividend_balances(database& db, auto itr = vesting_amounts.find(holder_balance_object.owner); if (itr != vesting_amounts.end()) holder_balance += itr->second; -// // working, but potential performance gap? -// auto vesting_range = vesting_index.indices().get().equal_range(holder_balance_object.owner); -// for (const vesting_balance_object& vesting_balance : boost::make_iterator_range(vesting_range.first, vesting_range.second)) -// { -// if (vesting_balance.balance.asset_id == dividend_holder_asset_obj.id) -// { -// holder_balance += vesting_balance.balance.amount; -// dlog("Vesting balances for account: ${owner}, amount: ${amount}", -// ("owner", vesting_balance.owner(db).name) -// ("amount", vesting_balance.balance.amount)); -// } -// } if (holder_balance.value) { fc::uint128_t amount_to_credit(delta_balance.value); @@ -1304,18 +1279,6 @@ void database::perform_chain_maintenance(const signed_block& next_block, const g auto itr = vesting_amounts.find(stake_account.id); if (itr != vesting_amounts.end()) voting_stake += itr->second.value; -// // working, but potential performance gap? -// auto vesting_range = d.get_index_type().indices().get().equal_range(stake_account.id); -// for (const vesting_balance_object& vesting_balance : boost::make_iterator_range(vesting_range.first, vesting_range.second)) -// { -// if (vesting_balance.balance.asset_id == asset_id_type()) -// { -// voting_stake += vesting_balance.balance.amount.value; -// dlog("Vote_tally_helper vesting balances for account: ${owner}, amount: ${amount}", -// ("owner", vesting_balance.owner(d).name) -// ("amount", vesting_balance.balance.amount)); -// } -// } for( vote_id_type id : opinion_account.options.votes ) { uint32_t offset = id.instance(); From 59c64efb5a85082aea3570931af648d3c3c674bf Mon Sep 17 00:00:00 2001 From: Roman Olearski Date: Sat, 26 Nov 2016 16:03:33 +0100 Subject: [PATCH 16/19] solution ...issues/#10 avoid generating dividend_payout virtual operations for zero-size payouts --- libraries/chain/db_maint.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index d83b68e0..2cb012e1 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -964,9 +964,10 @@ void schedule_pending_dividend_balances(database& db, } for (const auto& pending_payout : pending_payout_balance_index.indices()) - dlog("Pending payout: ${account_name} -> ${amount}", - ("account_name", pending_payout.owner(db).name) - ("amount", asset(pending_payout.pending_balance, pending_payout.dividend_payout_asset_type))); + if (pending_payout.pending_balance.value) + dlog("Pending payout: ${account_name} -> ${amount}", + ("account_name", pending_payout.owner(db).name) + ("amount", asset(pending_payout.pending_balance, pending_payout.dividend_payout_asset_type))); dlog("Remaining balance not paid out: ${amount}", ("amount", asset(remaining_amount_to_distribute, payout_asset_type))); @@ -1118,7 +1119,7 @@ void process_dividend_assets(database& db) { const pending_dividend_payout_balance_for_holder_object& pending_balance_object = *pending_balance_object_iter; - if (last_holder_account_id && *last_holder_account_id != pending_balance_object.owner) + if (last_holder_account_id && *last_holder_account_id != pending_balance_object.owner && payouts_for_this_holder.size()) { // we've moved on to a new account, generate the dividend payment virtual op for the previous one db.push_applied_operation(asset_dividend_distribution_operation(dividend_holder_asset_obj.id, @@ -1130,14 +1131,15 @@ void process_dividend_assets(database& db) } - if (is_authorized_asset(db, pending_balance_object.owner(db), pending_balance_object.dividend_payout_asset_type(db)) && + if (pending_balance_object.pending_balance.value && + is_authorized_asset(db, pending_balance_object.owner(db), pending_balance_object.dividend_payout_asset_type(db)) && is_asset_approved_for_distribution_account(pending_balance_object.dividend_payout_asset_type)) { dlog("Processing payout of ${asset} to account ${account}", ("asset", asset(pending_balance_object.pending_balance, pending_balance_object.dividend_payout_asset_type)) ("account", pending_balance_object.owner(db).name)); - db.adjust_balance(pending_balance_object.owner, + db.adjust_balance(pending_balance_object.owner, asset(pending_balance_object.pending_balance, pending_balance_object.dividend_payout_asset_type)); payouts_for_this_holder.insert(asset(pending_balance_object.pending_balance, @@ -1153,7 +1155,7 @@ void process_dividend_assets(database& db) ++pending_balance_object_iter; } // we will always be left with the last holder's data, generate the virtual op for it now. - if (last_holder_account_id) + if (last_holder_account_id && payouts_for_this_holder.size()) { // we've moved on to a new account, generate the dividend payment virtual op for the previous one db.push_applied_operation(asset_dividend_distribution_operation(dividend_holder_asset_obj.id, From 3a8b8a3b5b9a4689a62186e669d1753d59d83680 Mon Sep 17 00:00:00 2001 From: Roman Olearski Date: Mon, 28 Nov 2016 09:06:29 +0100 Subject: [PATCH 17/19] Cherry-picked commit b9508c6. little correction for solution ...issues/#10 avoid generating dividend_payout virtual operations for zero-size payouts --- libraries/chain/db_maint.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index 2cb012e1..19db3f3f 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -934,13 +934,15 @@ void schedule_pending_dividend_balances(database& db, auto itr = vesting_amounts.find(holder_balance_object.owner); if (itr != vesting_amounts.end()) holder_balance += itr->second; - if (holder_balance.value) + //if (holder_balance.value) + + fc::uint128_t amount_to_credit(delta_balance.value); + amount_to_credit *= holder_balance.value; + amount_to_credit /= total_balance_of_dividend_asset.value; + share_type shares_to_credit((int64_t)amount_to_credit.to_uint64()); + if (shares_to_credit.value) { - fc::uint128_t amount_to_credit(delta_balance.value); - amount_to_credit *= holder_balance.value; - amount_to_credit /= total_balance_of_dividend_asset.value; wdump((delta_balance.value)(holder_balance)(total_balance_of_dividend_asset)); - share_type shares_to_credit((int64_t)amount_to_credit.to_uint64()); remaining_amount_to_distribute -= shares_to_credit; From 32433ef29fde99fe05f88f729ca08708635f4f5c Mon Sep 17 00:00:00 2001 From: Roman Olearski Date: Thu, 6 Jul 2017 23:04:50 +0200 Subject: [PATCH 18/19] Cherry-picked all divident commits from rps to betting resolving conflicts --- libraries/chain/db_init.cpp | 4 ++-- libraries/chain/db_notify.cpp | 6 ++++++ .../chain/include/graphene/chain/protocol/operations.hpp | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index 9d2f5b7a..a6beca42 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -274,8 +274,8 @@ void database::initialize_indexes() add_index< primary_index< simple_index< fba_accumulator_object > > >(); add_index< primary_index< betting_market_position_index > >(); add_index< primary_index< global_betting_statistics_object_index > >(); - add_index< primary_index >(); - add_index< primary_index >(); + //add_index< primary_index >(); + //add_index< primary_index >(); add_index< primary_index >(); add_index< primary_index >(); } diff --git a/libraries/chain/db_notify.cpp b/libraries/chain/db_notify.cpp index a3660bc9..3713f4a4 100644 --- a/libraries/chain/db_notify.cpp +++ b/libraries/chain/db_notify.cpp @@ -73,6 +73,12 @@ struct get_impacted_account_visitor } void operator()( const asset_update_bitasset_operation& op ) {} + void operator()( const asset_update_dividend_operation& op ) {} + void operator()( const asset_dividend_distribution_operation& op ) + { + _impacted.insert( op.account_id ); + } + void operator()( const asset_update_feed_producers_operation& op ) {} void operator()( const asset_issue_operation& op ) diff --git a/libraries/chain/include/graphene/chain/protocol/operations.hpp b/libraries/chain/include/graphene/chain/protocol/operations.hpp index 5e6fe68d..9b83dc68 100644 --- a/libraries/chain/include/graphene/chain/protocol/operations.hpp +++ b/libraries/chain/include/graphene/chain/protocol/operations.hpp @@ -97,6 +97,8 @@ namespace graphene { namespace chain { asset_settle_cancel_operation, // VIRTUAL asset_claim_fees_operation, fba_distribute_operation, // VIRTUAL + asset_update_dividend_operation, + asset_dividend_distribution_operation, // VIRTUAL sport_create_operation, competitor_create_operation, event_group_create_operation, @@ -108,9 +110,7 @@ namespace graphene { namespace chain { betting_market_group_resolved_operation, // VIRTUAL bet_matched_operation, // VIRTUAL bet_cancel_operation, - bet_canceled_operation, // VIRTUAL - asset_update_dividend_operation, - asset_dividend_distribution_operation // VIRTUAL + bet_canceled_operation // VIRTUAL > operation; /// @} // operations group From c18094abd8f05ef95e7f75081c78931d890f92f2 Mon Sep 17 00:00:00 2001 From: Roman Olearski Date: Fri, 7 Jul 2017 14:23:02 +0200 Subject: [PATCH 19/19] implementing rake fee in resolve_betting_market_group #1 improving net profit calculation --- libraries/chain/db_bet.cpp | 51 ++++++++++++++++--- libraries/chain/db_init.cpp | 8 +-- .../chain/include/graphene/chain/config.hpp | 4 +- .../chain/protocol/chain_parameters.hpp | 3 +- tests/betting/betting_tests.cpp | 21 ++++++-- 5 files changed, 71 insertions(+), 16 deletions(-) diff --git a/libraries/chain/db_bet.cpp b/libraries/chain/db_bet.cpp index 776640d9..39999b38 100644 --- a/libraries/chain/db_bet.cpp +++ b/libraries/chain/db_bet.cpp @@ -92,6 +92,8 @@ void database::resolve_betting_market_group(const betting_market_group_object& b // walking through bettors' positions and collecting winings and fees respecting asset_id for (const auto& bettor_positions_pair: bettor_positions_map) { + uint16_t rake_fee_percentage = get_global_properties().parameters.betting_rake_fee_percentage; + std::map net_profits; std::map payout_amounts; std::map fees_collected; account_id_type bettor_id = bettor_positions_pair.first; @@ -118,11 +120,17 @@ void database::resolve_betting_market_group(const betting_market_group_object& b payout_amounts[betting_market.asset_id] += position->pay_if_payout_condition; payout_amounts[betting_market.asset_id] += position->pay_if_not_canceled; fees_collected[betting_market.asset_id] += position->fees_collected; + net_profits[betting_market.asset_id] += position->pay_if_payout_condition; + net_profits[betting_market.asset_id] += position->pay_if_not_canceled; + net_profits[betting_market.asset_id] -= position->pay_if_canceled; break; case betting_market_resolution_type::not_win: payout_amounts[betting_market.asset_id] += position->pay_if_not_payout_condition; payout_amounts[betting_market.asset_id] += position->pay_if_not_canceled; fees_collected[betting_market.asset_id] += position->fees_collected; + net_profits[betting_market.asset_id] += position->pay_if_not_payout_condition; + net_profits[betting_market.asset_id] += position->pay_if_not_canceled; + net_profits[betting_market.asset_id] -= position->pay_if_canceled; break; case betting_market_resolution_type::cancel: payout_amounts[betting_market.asset_id] += position->pay_if_canceled; @@ -138,15 +146,44 @@ void database::resolve_betting_market_group(const betting_market_group_object& b std::vector fees; for (const auto& payout_amount_pair: payout_amounts) { - asset payout = asset(payout_amount_pair.second, payout_amount_pair.first); + // pay the fees to the correct (dividend-distribution) account if net profit + asset_id_type asset_id = payout_amount_pair.first; + const asset_object & asset_obj = asset_id(*this); + optional dividend_id = asset_obj.dividend_data_id; + account_id_type rake_account_id; + if (dividend_id.valid()) + { + const asset_dividend_data_id_type& asset_dividend_data_id_= *dividend_id; + const asset_dividend_data_object& dividend_obj = asset_dividend_data_id_(*this); + rake_account_id = dividend_obj.dividend_distribution_account; + } + asset fee = fees_collected[asset_id]; + share_type net_profit = net_profits[asset_id]; + share_type payout_amount = payout_amount_pair.second; + share_type rake_amount = 0; + if (dividend_id.valid()) + { + if (net_profit.value > 0) + { + rake_amount = (fc::uint128_t(net_profit.value) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100).to_uint64(); + if (rake_amount.value) + { + // adjusting balance of dividend_distribution_account + asset rake(rake_amount, asset_id); + adjust_balance(rake_account_id, rake); + } + } + adjust_balance(rake_account_id, fee); + } + else + { + adjust_balance(account_id_type(), fee); + } + // pay winning - rake + asset payout = asset(payout_amount - rake_amount, asset_id); adjust_balance(bettor_id, payout); + winnings.push_back(payout); - } - for (const auto& fee_collected_pair: fees_collected) - { - // TODO : pay the fees to the correct (dividend-distribution) account - asset fee = asset(fee_collected_pair.second, fee_collected_pair.first); - adjust_balance(account_id_type(), fee); fees.push_back(fee); } push_applied_operation(betting_market_group_resolved_operation(bettor_id, diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index a6beca42..53c2c1a6 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -381,7 +381,7 @@ void database::init_genesis(const genesis_state_type& genesis_state) a.membership_expiration_date = time_point_sec::maximum(); a.network_fee_percentage = 0; a.lifetime_referrer_fee_percentage = GRAPHENE_100_PERCENT; - }).get_id() == TOURNAMENT_RAKE_FEE_ACCOUNT_ID); + }).get_id() == GRAPHENE_RAKE_FEE_ACCOUNT_ID); // Create more special accounts while( true ) @@ -415,7 +415,7 @@ void database::init_genesis(const genesis_state_type& genesis_state) a.options.minimum_fee_percentage = 10*GRAPHENE_1_PERCENT; a.options.next_payout_time = genesis_state.initial_timestamp + fc::hours(1); a.options.payout_interval = 7*24*60*60; - a.dividend_distribution_account = TOURNAMENT_RAKE_FEE_ACCOUNT_ID; + a.dividend_distribution_account = GRAPHENE_RAKE_FEE_ACCOUNT_ID; }); const asset_object& core_asset = @@ -447,7 +447,7 @@ void database::init_genesis(const genesis_state_type& genesis_state) a.options.minimum_fee_percentage = 10*GRAPHENE_1_PERCENT; a.options.next_payout_time = genesis_state.initial_timestamp + fc::hours(1); a.options.payout_interval = 7*24*60*60; - a.dividend_distribution_account = TOURNAMENT_RAKE_FEE_ACCOUNT_ID; + a.dividend_distribution_account = GRAPHENE_RAKE_FEE_ACCOUNT_ID; }); const asset_object& default_asset = @@ -458,7 +458,7 @@ void database::init_genesis(const genesis_state_type& genesis_state) a.precision = GRAPHENE_BLOCKCHAIN_PRECISION_DIGITS; a.options.flags = 0; a.options.issuer_permissions = 79; - a.issuer = TOURNAMENT_RAKE_FEE_ACCOUNT_ID; + a.issuer = GRAPHENE_RAKE_FEE_ACCOUNT_ID; a.options.core_exchange_rate.base.amount = 1; a.options.core_exchange_rate.base.asset_id = asset_id_type(0); a.options.core_exchange_rate.quote.amount = 1; diff --git a/libraries/chain/include/graphene/chain/config.hpp b/libraries/chain/include/graphene/chain/config.hpp index 4cc147ac..e70c2214 100644 --- a/libraries/chain/include/graphene/chain/config.hpp +++ b/libraries/chain/include/graphene/chain/config.hpp @@ -165,13 +165,15 @@ /// Represents the canonical account for specifying you will vote directly (as opposed to a proxy) #define GRAPHENE_PROXY_TO_SELF_ACCOUNT (graphene::chain::account_id_type(5)) /// -#define TOURNAMENT_RAKE_FEE_ACCOUNT_ID (graphene::chain::account_id_type(6)) +#define GRAPHENE_RAKE_FEE_ACCOUNT_ID (graphene::chain::account_id_type(6)) /// Sentinel value used in the scheduler. #define GRAPHENE_NULL_WITNESS (graphene::chain::witness_id_type(0)) ///@} #define GRAPHENE_FBA_STEALTH_DESIGNATED_ASSET (asset_id_type(743)) +#define GRAPHENE_DEFAULT_RAKE_FEE_PERCENTAGE (3*GRAPHENE_1_PERCENT) + /** * Betting-related constants. * diff --git a/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp b/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp index 5252f982..b355271f 100644 --- a/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp +++ b/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp @@ -69,7 +69,7 @@ namespace graphene { namespace chain { uint16_t accounts_per_fee_scale = GRAPHENE_DEFAULT_ACCOUNTS_PER_FEE_SCALE; ///< number of accounts between fee scalings uint8_t account_fee_scale_bitshifts = GRAPHENE_DEFAULT_ACCOUNT_FEE_SCALE_BITSHIFTS; ///< number of times to left bitshift account registration fee at each scaling uint8_t max_authority_depth = GRAPHENE_MAX_SIG_CHECK_DEPTH; - + uint16_t betting_rake_fee_percentage = GRAPHENE_DEFAULT_RAKE_FEE_PERCENTAGE; ///< part of prize paid into the dividend account for the core token holders bet_multiplier_type min_bet_multiplier = GRAPHENE_DEFAULT_MIN_BET_MULTIPLIER; bet_multiplier_type max_bet_multiplier = GRAPHENE_DEFAULT_MAX_BET_MULTIPLIER; flat_map permitted_betting_odds_increments = GRAPHENE_DEFAULT_PERMITTED_BETTING_ODDS_INCREMENTS; @@ -113,6 +113,7 @@ FC_REFLECT( graphene::chain::chain_parameters, (max_authority_depth) (min_bet_multiplier) (max_bet_multiplier) + (betting_rake_fee_percentage) (permitted_betting_odds_increments) (extensions) ) diff --git a/tests/betting/betting_tests.cpp b/tests/betting/betting_tests.cpp index c8d56b6c..999c824e 100644 --- a/tests/betting/betting_tests.cpp +++ b/tests/betting/betting_tests.cpp @@ -118,7 +118,11 @@ BOOST_AUTO_TEST_CASE( peerplays_sport_create_test ) {{capitals_win_market.id, betting_market_resolution_type::win}, {blackhawks_win_market.id, betting_market_resolution_type::cancel}}); - BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 - 20000 + 2000000); + + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 - 20000 + 2000000 - rake_value); BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000 - 20000); } FC_LOG_AND_RETHROW() @@ -294,10 +298,16 @@ BOOST_AUTO_TEST_CASE( win ) GET_ACTOR(alice); GET_ACTOR(bob); + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + uint32_t rake_value; + //rake_value = (-100 + 1100 - 1100) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; // alice starts with 10000, pays 100 (bet) + 2 (fee), wins 1100, then pays 1100 (bet) + 22 (fee), wins 0 BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000 - 100 - 2 + 1100 - 1100 - 22 + 0); + + rake_value = (-1000 - 1100 + 2200) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; // bob starts with 10000, pays 1000 (bet) + 20 (fee), wins 0, then pays 1100 (bet) + 22 (fee), wins 2200 - BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000 - 1000 - 20 + 0 - 1100 - 22 + 2200); + BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000 - 1000 - 20 + 0 - 1100 - 22 + 2200 - rake_value); } FC_LOG_AND_RETHROW() } @@ -312,8 +322,13 @@ BOOST_AUTO_TEST_CASE( not_win ) GET_ACTOR(alice); GET_ACTOR(bob); + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + uint32_t rake_value = (-100 - 1100 + 2200) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; // alice starts with 10000, pays 100 (bet) + 2 (fee), wins 0, then pays 1100 (bet) + 22 (fee), wins 2200 - BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000 - 100 - 2 + 0 - 1100 - 22 + 2200); + BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000 - 100 - 2 + 0 - 1100 - 22 + 2200 - rake_value); + + //rake_value = (-1000 + 1100 - 1100) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; // bob starts with 10000, pays 1000 (bet) + 20 (fee), wins 1100, then pays 1100 (bet) + 22 (fee), wins 0 BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000 - 1000 - 20 + 1100 - 1100 - 22 + 0); } FC_LOG_AND_RETHROW()