Fixes to paying out non-core assets using their fee pools

This commit is contained in:
Eric Frias 2016-08-25 10:41:01 -04:00
parent 6c35e8d2a3
commit bc212b7d59
7 changed files with 415 additions and 143 deletions

View file

@ -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();

View file

@ -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<by_account_asset>().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<by_asset_balance>().lower_bound(boost::make_tuple(dividend_holder_asset_obj.id));
auto holder_balances_end =
balance_index.indices().get<by_asset_balance>().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<asset_dividend_distribution_operation>().distribution_base_fee;
uint32_t distribution_fee_per_holder = gpo.parameters.current_fees->get<asset_dividend_distribution_operation>().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<by_asset_balance>().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<asset_index>();
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<by_dividend_payout_account>().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<by_dividend_payout_account>().end())
db.create<pending_dividend_payout_balance_object>( [&]( 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<by_dividend_payout_account>().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<by_dividend_payout_account>().end())
db.create<pending_dividend_payout_balance_object>( [&]( 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>( [&]( 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<by_dividend_payout_account>().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>( [&]( 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>( [&]( 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<by_dividend_payout_account>().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>( [&]( 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<fc::time_point_sec> 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));
});
}
}

View file

@ -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<time_point_sec> 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<time_point_sec> 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<time_point_sec> 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<time_point_sec> last_distribution_time;
/// The account which collects pending payouts
account_id_type dividend_distribution_account;
};

View file

@ -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<uint32_t> 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<uint32_t> 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)

View file

@ -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)

View file

@ -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<string> new_feed_producers,
bool broadcast /* = false */)

View file

@ -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<asset_dividend_distribution_operation>().distribution_base_fee = 100;
// _gpo.parameters.current_fees->get<asset_dividend_distribution_operation>().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<uint32_t>();
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<uint32_t>();
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;