Cherry-picked commit abc7853.

Initial work on dividend-paying assets.  Basic functionality works in simple cases.
This commit is contained in:
Eric Frias 2016-06-26 15:41:07 -04:00 committed by Roman Olearski
parent 9b08b502be
commit b9304caffa
15 changed files with 863 additions and 5 deletions

View file

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

View file

@ -32,6 +32,8 @@
#include <functional>
#include <boost/algorithm/string/case_conv.hpp>
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>( [&]( 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>([&](account_statistics_object& s){s.owner = obj.id;}).id;
});
const asset_dividend_data_object& dividend_data = d.create<asset_dividend_data_object>( [&]( 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();

View file

@ -183,6 +183,7 @@ void database::initialize_evaluators()
register_evaluator<asset_reserve_evaluator>();
register_evaluator<asset_update_evaluator>();
register_evaluator<asset_update_bitasset_evaluator>();
register_evaluator<asset_update_dividend_evaluator>();
register_evaluator<asset_update_feed_producers_evaluator>();
register_evaluator<asset_settle_evaluator>();
register_evaluator<asset_global_settle_evaluator>();
@ -259,6 +260,7 @@ void database::initialize_indexes()
add_index< primary_index<transaction_index > >();
add_index< primary_index<account_balance_index > >();
add_index< primary_index<asset_bitasset_data_index > >();
add_index< primary_index<asset_dividend_data_object_index > >();
add_index< primary_index<simple_index<global_property_object >> >();
add_index< primary_index<simple_index<dynamic_global_property_object >> >();
add_index< primary_index<simple_index<account_statistics_object >> >();
@ -269,10 +271,11 @@ void database::initialize_indexes()
add_index< primary_index<simple_index<budget_record_object > > >();
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<pending_dividend_payout_balance_object_index > >();
add_index< primary_index<distributed_dividend_balance_object_index > >();
}
void database::init_genesis(const genesis_state_type& genesis_state)

View file

@ -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<by_account_asset>().equal_range(boost::make_tuple(dividend_data.dividend_distribution_account));
auto previous_distribution_account_balance_range =
distributed_dividend_balance_index.indices().get<by_dividend_payout_asset>().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<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;
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<by_dividend_asset_account_asset>().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_asset_account_asset>().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", 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>( [&]( 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<by_dividend_asset_account_asset>().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;
});
}
// 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<account_balance_index>();
const distributed_dividend_balance_object_index& distributed_dividend_balance_index = db.get_index_type<distributed_dividend_balance_object_index>();
const pending_dividend_payout_balance_object_index& pending_payout_balance_index = db.get_index_type<pending_dividend_payout_balance_object_index>();
// 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<asset_index>().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<account_balance_index>().indices().get<by_account_asset>();
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<asset_id_type, share_type> amounts_paid_out_by_asset;
#endif
auto pending_payouts_range =
pending_payout_balance_index.indices().get<by_dividend_asset_account_asset>().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<by_dividend_payout_asset>().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<fc::time_point_sec> 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;

View file

@ -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<account_id_type> > 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<pending_dividend_payout_balance_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_object, account_multi_index_type> 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<by_id>, member< object, object_id_type, &object::id > >,
ordered_unique< tag<by_dividend_asset_account_asset>,
composite_key<
pending_dividend_payout_balance_object,
member<pending_dividend_payout_balance_object, asset_id_type, &pending_dividend_payout_balance_object::dividend_holder_asset_type>,
member<pending_dividend_payout_balance_object, asset_id_type, &pending_dividend_payout_balance_object::dividend_payout_asset_type>,
member<pending_dividend_payout_balance_object, account_id_type, &pending_dividend_payout_balance_object::owner>
>
>
>
> pending_dividend_payout_balance_object_multi_index_type;
/**
* @ingroup object_index
*/
typedef generic_index<pending_dividend_payout_balance_object, pending_dividend_payout_balance_object_multi_index_type> 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) )

View file

@ -82,6 +82,18 @@ namespace graphene { namespace chain {
const asset_bitasset_data_object* bitasset_to_update = nullptr;
};
class asset_update_dividend_evaluator : public evaluator<asset_update_dividend_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<asset_update_feed_producers_evaluator>
{
public:

View file

@ -132,6 +132,9 @@ namespace graphene { namespace chain {
optional<account_id_type> buyback_account;
/// Extra data associated with dividend-paying assets.
optional<asset_dividend_data_id_type> 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<class DB>
const asset_dividend_data_object& dividend_data(const DB& db)const
{ assert(dividend_data_id); return db.get(*dividend_data_id); }
template<class DB>
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_object, asset_object_multi_index_type> asset_index;
/**
* @brief contains properties that only apply to dividend-paying assets
*
* @ingroup object
* @ingroup implementation
*/
class asset_dividend_data_object : public abstract_object<asset_dividend_data_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<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)
fc::optional<time_point_sec> 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<by_id>, member< object, object_id_type, &object::id > >
>
> asset_dividend_data_object_multi_index_type;
typedef generic_index<asset_dividend_data_object, asset_dividend_data_object_multi_index_type> 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<distributed_dividend_balance_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<by_id>, member< object, object_id_type, &object::id > >,
ordered_unique< tag<by_dividend_payout_asset>,
composite_key<
distributed_dividend_balance_object,
member<distributed_dividend_balance_object, asset_id_type, &distributed_dividend_balance_object::dividend_holder_asset_type>,
member<distributed_dividend_balance_object, asset_id_type, &distributed_dividend_balance_object::dividend_payout_asset_type>
>
>
>
> distributed_dividend_balance_object_multi_index_type;
typedef generic_index<distributed_dividend_balance_object, distributed_dividend_balance_object_multi_index_type> 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)

View file

@ -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<fc::time_point_sec> 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<uint32_t> 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)
)

View file

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

View file

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

View file

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

View file

@ -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_object> 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<string> 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<operation_detail> 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();
}

View file

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

View file

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

View file

@ -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<pending_dividend_payout_balance_object_index>().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<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;
}
}
BOOST_AUTO_TEST_CASE( cancel_limit_order_test )
{ try {
INVOKE( issue_uia );