From 06a9488f8b2df6572e196821b9a4bfc669834ae6 Mon Sep 17 00:00:00 2001 From: Daniel Larimer Date: Tue, 23 Jun 2015 10:12:53 -0400 Subject: [PATCH] Issue #53 - flexible vesting balance creation Use static variant to allow the types of vesting balances to be easily extended and the creation operation allows for many different types of initialization parameters. Added a check that requires a minimum claim date which allows creating of vesting balance objects with a cliff. --- .../include/graphene/chain/operations.hpp | 39 +++++++++++++--- .../include/graphene/chain/predicate.hpp | 4 ++ .../graphene/chain/vesting_balance_object.hpp | 43 +++++++++++++---- libraries/chain/operations.cpp | 1 - libraries/chain/vesting_balance_evaluator.cpp | 46 ++++++++++++++----- libraries/chain/vesting_balance_object.cpp | 4 ++ tests/tests/operation_tests.cpp | 9 ++-- 7 files changed, 112 insertions(+), 34 deletions(-) diff --git a/libraries/chain/include/graphene/chain/operations.hpp b/libraries/chain/include/graphene/chain/operations.hpp index 8a322a50..05187a10 100644 --- a/libraries/chain/include/graphene/chain/operations.hpp +++ b/libraries/chain/include/graphene/chain/operations.hpp @@ -25,6 +25,10 @@ #include #include #include +#include + +/// TODO: why does this file depend upon database objects? +/// we should remove these headers #include #include #include @@ -1184,6 +1188,25 @@ namespace graphene { namespace chain { } }; + struct linear_vesting_policy_initializer + { + /** while vesting begins on begin_date, none may be claimed before the start_claim time */ + fc::time_point_sec start_claim; + fc::time_point_sec begin_date; + uint32_t vesting_seconds = 0; + }; + + struct ccd_vesting_policy_initializer + { + /** while coindays may accrue over time, none may be claimed before the start_claim time */ + fc::time_point_sec start_claim; + uint32_t vesting_seconds = 0; + ccd_vesting_policy_initializer( uint32_t vest_sec = 0, fc::time_point_sec sc = fc::time_point_sec() ):start_claim(sc),vesting_seconds(vest_sec){} + }; + + typedef fc::static_variant< linear_vesting_policy_initializer, ccd_vesting_policy_initializer > vesting_policy_initializer; + + /** * @brief Create a vesting balance. * @ingroup operations @@ -1203,11 +1226,11 @@ namespace graphene { namespace chain { */ struct vesting_balance_create_operation { - asset fee; - account_id_type creator; ///< Who provides funds initially - account_id_type owner; ///< Who is able to withdraw the balance - asset amount; - uint32_t vesting_seconds; + asset fee; + account_id_type creator; ///< Who provides funds initially + account_id_type owner; ///< Who is able to withdraw the balance + asset amount; + vesting_policy_initializer policy; account_id_type fee_payer()const { return creator; } void get_required_auth(flat_set& active_auth_set, flat_set&)const; @@ -1220,6 +1243,7 @@ namespace graphene { namespace chain { } }; + /** * @brief Withdraw from a vesting balance. * @ingroup operations @@ -1585,7 +1609,7 @@ FC_REFLECT( graphene::chain::withdraw_permission_claim_operation, (fee)(withdraw FC_REFLECT( graphene::chain::withdraw_permission_delete_operation, (fee)(withdraw_from_account)(authorized_account) (withdrawal_permission) ) -FC_REFLECT( graphene::chain::vesting_balance_create_operation, (fee)(creator)(owner)(amount)(vesting_seconds) ) +FC_REFLECT( graphene::chain::vesting_balance_create_operation, (fee)(creator)(owner)(amount)(policy) ) FC_REFLECT( graphene::chain::vesting_balance_withdraw_operation, (fee)(vesting_balance)(owner)(amount) ) FC_REFLECT( graphene::chain::worker_create_operation, @@ -1599,3 +1623,6 @@ FC_REFLECT( graphene::chain::void_result, ) FC_REFLECT_TYPENAME( graphene::chain::operation ) FC_REFLECT_TYPENAME( graphene::chain::operation_result ) FC_REFLECT_TYPENAME( fc::flat_set ) +FC_REFLECT(graphene::chain::linear_vesting_policy_initializer, (start_claim)(begin_date)(vesting_seconds) ) +FC_REFLECT(graphene::chain::ccd_vesting_policy_initializer, (start_claim)(vesting_seconds) ) +FC_REFLECT_TYPENAME( graphene::chain::vesting_policy_initializer ) diff --git a/libraries/chain/include/graphene/chain/predicate.hpp b/libraries/chain/include/graphene/chain/predicate.hpp index a05d51ab..120caf4b 100644 --- a/libraries/chain/include/graphene/chain/predicate.hpp +++ b/libraries/chain/include/graphene/chain/predicate.hpp @@ -21,6 +21,9 @@ namespace graphene { namespace chain { +bool is_valid_symbol( const string& symbol ); +bool is_valid_name( const string& s ); + class database; /** @@ -66,4 +69,5 @@ typedef static_variant< FC_REFLECT( graphene::chain::verify_account_name, (account_id)(account_name) ) FC_REFLECT( graphene::chain::verify_symbol, (asset_id)(symbol) ) +FC_REFLECT_TYPENAME( graphene::chain::predicate ) diff --git a/libraries/chain/include/graphene/chain/vesting_balance_object.hpp b/libraries/chain/include/graphene/chain/vesting_balance_object.hpp index 22c10b67..d8019d7d 100644 --- a/libraries/chain/include/graphene/chain/vesting_balance_object.hpp +++ b/libraries/chain/include/graphene/chain/vesting_balance_object.hpp @@ -38,20 +38,30 @@ namespace graphene { namespace chain { asset _amount) : balance(_balance), now(_now), amount(_amount) {} - asset balance; + asset balance; fc::time_point_sec now; - asset amount; + asset amount; }; /** - * Linear vesting balance. + * @brief Linear vesting balance with cliff + * + * This vesting balance type is used to mimic traditional stock vesting contracts where + * each day a certain amount vests until it is fully matured. + * + * @note New funds may not be added to a linear vesting balance. */ struct linear_vesting_policy { - uint32_t vesting_seconds; // must be greater than zero + uint32_t vesting_seconds = 0; ///< must be greater than zero + /** while coindays may accrue over time, none may be claimed before first_claim date */ + fc::time_point_sec start_claim; + /** linear vesting may begin prior to allowing the user to actually claim the funds, this + * can be used to create a cliff. + */ fc::time_point_sec begin_date; - share_type begin_balance; // same asset as balance - share_type total_withdrawn; // same asset as balance + share_type begin_balance; ///< same asset as balance + share_type total_withdrawn; ///< same asset as balance asset get_allowed_withdraw(const vesting_policy_context& ctx)const; bool is_deposit_allowed(const vesting_policy_context& ctx)const; @@ -63,10 +73,20 @@ namespace graphene { namespace chain { void on_withdraw(const vesting_policy_context& ctx); }; + + /** + * @brief defines vesting in terms of coin-days accrued which allows for dynamic deposit/withdraw + * + * The economic effect of this vesting policy is to require a certain amount of "interest" to accrue + * before the full balance may be withdrawn. Interest accrues as coindays (balance * length held). If + * some of the balance is withdrawn, the remaining balance must be held longer. + */ struct cdd_vesting_policy { - uint32_t vesting_seconds; + uint32_t vesting_seconds = 0; fc::uint128_t coin_seconds_earned; + /** while coindays may accrue over time, none may be claimed before first_claim date */ + fc::time_point_sec start_claim; fc::time_point_sec coin_seconds_earned_last_update; /** @@ -97,6 +117,7 @@ namespace graphene { namespace chain { cdd_vesting_policy > vesting_policy; + /** * Vesting balance object is a balance that is locked by the blockchain for a period of time. */ @@ -106,9 +127,9 @@ namespace graphene { namespace chain { static const uint8_t space_id = protocol_ids; static const uint8_t type_id = vesting_balance_object_type; - account_id_type owner; - asset balance; - vesting_policy policy; + account_id_type owner; + asset balance; + vesting_policy policy; vesting_balance_object() {} @@ -138,6 +159,7 @@ namespace graphene { namespace chain { FC_REFLECT(graphene::chain::linear_vesting_policy, (vesting_seconds) + (start_claim) (begin_date) (begin_balance) (total_withdrawn) @@ -145,6 +167,7 @@ FC_REFLECT(graphene::chain::linear_vesting_policy, FC_REFLECT(graphene::chain::cdd_vesting_policy, (vesting_seconds) + (start_claim) (coin_seconds_earned) (coin_seconds_earned_last_update) ) diff --git a/libraries/chain/operations.cpp b/libraries/chain/operations.cpp index 30cf15d3..6167f17d 100644 --- a/libraries/chain/operations.cpp +++ b/libraries/chain/operations.cpp @@ -697,7 +697,6 @@ void vesting_balance_create_operation::validate()const { FC_ASSERT( fee.amount >= 0 ); FC_ASSERT( amount.amount > 0 ); - FC_ASSERT( vesting_seconds > 0 ); } void vesting_balance_withdraw_operation::get_required_auth(flat_set& active_auth_set, flat_set&)const diff --git a/libraries/chain/vesting_balance_evaluator.cpp b/libraries/chain/vesting_balance_evaluator.cpp index af521f80..4d1d7ed2 100644 --- a/libraries/chain/vesting_balance_evaluator.cpp +++ b/libraries/chain/vesting_balance_evaluator.cpp @@ -36,17 +36,48 @@ object_id_type vesting_balance_create_evaluator::do_evaluate( const vesting_bala FC_ASSERT( d.get_balance( creator_account.id, op.amount.asset_id ) >= op.amount ); FC_ASSERT( !op.amount.asset_id(d).is_transfer_restricted() ); - /** we cannot create vesting balances that are market issued due to black swans */ - FC_ASSERT( !op.amount.asset_id(d).is_market_issued() ); - return object_id_type(); } +struct init_policy_visitor +{ + typedef void result_type; + + init_policy_visitor( vesting_policy& po, + const share_type& begin_balance, + const fc::time_point_sec& n ):p(po),init_balance(begin_balance),now(n){} + + vesting_policy& p; + share_type init_balance; + fc::time_point_sec now; + + void operator()( const linear_vesting_policy_initializer& i )const + { + linear_vesting_policy policy; + policy.vesting_seconds = i.vesting_seconds; + policy.begin_date = i.begin_date; + policy.start_claim = i.start_claim; + policy.begin_balance = init_balance; + p = policy; + } + + void operator()( const ccd_vesting_policy_initializer& i )const + { + cdd_vesting_policy policy; + policy.vesting_seconds = i.vesting_seconds; + policy.start_claim = i.start_claim; + policy.coin_seconds_earned = 0; + policy.coin_seconds_earned_last_update = now; + p = policy; + } +}; + object_id_type vesting_balance_create_evaluator::do_apply( const vesting_balance_create_operation& op ) { database& d = db(); const time_point_sec now = d.head_block_time(); + FC_ASSERT( d.get_balance( op.creator, op.amount.asset_id ) >= op.amount ); d.adjust_balance( op.creator, -op.amount ); const vesting_balance_object& vbo = d.create< vesting_balance_object >( [&]( vesting_balance_object& obj ) @@ -55,16 +86,9 @@ object_id_type vesting_balance_create_evaluator::do_apply( const vesting_balance // If making changes to this logic, check if those changes should also be made there as well. obj.owner = op.owner; obj.balance = op.amount; - - cdd_vesting_policy policy; - policy.vesting_seconds = op.vesting_seconds; - policy.coin_seconds_earned = 0; - policy.coin_seconds_earned_last_update = now; - - obj.policy = policy; + op.policy.visit( init_policy_visitor( obj.policy, op.amount.amount, now ) ); } ); - FC_ASSERT( d.get_balance( op.creator, op.amount.asset_id ) >= op.amount ); return vbo.id; } diff --git a/libraries/chain/vesting_balance_object.cpp b/libraries/chain/vesting_balance_object.cpp index bfc91de5..f57920c2 100644 --- a/libraries/chain/vesting_balance_object.cpp +++ b/libraries/chain/vesting_balance_object.cpp @@ -31,6 +31,8 @@ inline bool sum_below_max_shares(const asset& a, const asset& b) asset linear_vesting_policy::get_allowed_withdraw(const vesting_policy_context& ctx) const { + if(ctx.now <= start_claim) + return asset(0, ctx.balance.asset_id); if(ctx.now <= begin_date) return asset(0, ctx.balance.asset_id); if(vesting_seconds == 0) @@ -96,6 +98,8 @@ void cdd_vesting_policy::update_coin_seconds_earned(const vesting_policy_context asset cdd_vesting_policy::get_allowed_withdraw(const vesting_policy_context& ctx)const { + if(ctx.now <= start_claim) + return asset(0, ctx.balance.asset_id); fc::uint128_t cs_earned = compute_coin_seconds_earned(ctx); fc::uint128_t withdraw_available = cs_earned / vesting_seconds; assert(withdraw_available <= ctx.balance.amount.value); diff --git a/tests/tests/operation_tests.cpp b/tests/tests/operation_tests.cpp index b96088d6..0ddde5e4 100644 --- a/tests/tests/operation_tests.cpp +++ b/tests/tests/operation_tests.cpp @@ -1395,7 +1395,8 @@ BOOST_AUTO_TEST_CASE( vesting_balance_create_test ) op.creator = account_id_type(); op.owner = account_id_type(); op.amount = test_asset.amount( 100 ); - op.vesting_seconds = 60*60*24; + //op.vesting_seconds = 60*60*24; + op.policy = ccd_vesting_policy_initializer{ 60*60*24 }; // Fee must be non-negative REQUIRE_OP_VALIDATION_SUCCESS( op, fee, core.amount( 1 ) ); @@ -1407,10 +1408,6 @@ BOOST_AUTO_TEST_CASE( vesting_balance_create_test ) REQUIRE_OP_VALIDATION_FAILURE( op, amount, core.amount( 0 ) ); REQUIRE_OP_VALIDATION_FAILURE( op, amount, core.amount( -1 ) ); - // Min vesting period must be at least 1 sec - REQUIRE_OP_VALIDATION_SUCCESS( op, vesting_seconds, 1 ); - REQUIRE_OP_VALIDATION_FAILURE( op, vesting_seconds, 0 ); - // Setup world state we will need to test actual evaluation const account_object& alice_account = create_account( "alice" ); const account_object& bob_account = create_account( "bob" ); @@ -1488,7 +1485,7 @@ BOOST_AUTO_TEST_CASE( vesting_balance_withdraw_test ) create_op.creator = creator; create_op.owner = owner; create_op.amount = amount; - create_op.vesting_seconds = vesting_seconds; + create_op.policy = ccd_vesting_policy_initializer( vesting_seconds ); tx.operations.push_back( create_op ); processed_transaction ptx = PUSH_TX( db, tx, ~0 );