From bc212b7d59bd46dee38b780b474f7ba41b4c8001 Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Thu, 25 Aug 2016 10:41:01 -0400 Subject: [PATCH] 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 f7405366..7af71e44 100644 --- a/libraries/chain/asset_evaluator.cpp +++ b/libraries/chain/asset_evaluator.cpp @@ -411,6 +411,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 db3e386b..c284e0f7 100644 --- a/libraries/chain/include/graphene/chain/asset_object.hpp +++ b/libraries/chain/include/graphene/chain/asset_object.hpp @@ -270,12 +270,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 b171d5c7..1e4d40a8 100644 --- a/libraries/wallet/include/graphene/wallet/wallet.hpp +++ b/libraries/wallet/include/graphene/wallet/wallet.hpp @@ -1001,6 +1001,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. @@ -1579,6 +1594,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 d7e9c1d1..6797b1e5 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -3194,6 +3194,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;