From 5f79b662dcb28f9ab3e9fa5424f03ad73be185a0 Mon Sep 17 00:00:00 2001 From: Vikram Rajkumar Date: Mon, 15 Jun 2015 15:10:40 -0400 Subject: [PATCH] Revert "Remove bond operations" This reverts commit d12d9cdb0c802a1c8754a0a5f1c39cbe72008218. --- libraries/chain/CMakeLists.txt | 1 + libraries/chain/bond_evaluator.cpp | 218 ++++++++++++++++++ libraries/chain/db_init.cpp | 8 + libraries/chain/db_market.cpp | 34 +++ .../include/graphene/chain/bond_evaluator.hpp | 68 ++++++ .../include/graphene/chain/bond_object.hpp | 111 +++++++++ .../include/graphene/chain/operations.hpp | 126 ++++++++++ .../chain/include/graphene/chain/types.hpp | 18 ++ libraries/chain/operations.cpp | 71 ++++++ .../account_history_plugin.cpp | 12 + libraries/wallet/cache.cpp | 5 + programs/js_operation_serializer/main.cpp | 1 + tests/common/database_fixture.cpp | 7 + tests/tests/operation_tests.cpp | 64 +++++ 14 files changed, 744 insertions(+) create mode 100644 libraries/chain/bond_evaluator.cpp create mode 100644 libraries/chain/include/graphene/chain/bond_evaluator.hpp create mode 100644 libraries/chain/include/graphene/chain/bond_object.hpp diff --git a/libraries/chain/CMakeLists.txt b/libraries/chain/CMakeLists.txt index 17635897..4d4c3abe 100644 --- a/libraries/chain/CMakeLists.txt +++ b/libraries/chain/CMakeLists.txt @@ -18,6 +18,7 @@ add_library( graphene_chain proposal_evaluator.cpp short_order_evaluator.cpp limit_order_evaluator.cpp + bond_evaluator.cpp vesting_balance_evaluator.cpp withdraw_permission_evaluator.cpp worker_evaluator.cpp diff --git a/libraries/chain/bond_evaluator.cpp b/libraries/chain/bond_evaluator.cpp new file mode 100644 index 00000000..c3108771 --- /dev/null +++ b/libraries/chain/bond_evaluator.cpp @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2015, Cryptonomex, Inc. + * All rights reserved. + * + * This source code is provided for evaluation in private test networks only, until September 8, 2015. After this date, this license expires and + * the code may not be used, modified or distributed for any purpose. Redistribution and use in source and binary forms, with or without modification, + * are permitted until September 8, 2015, provided that the following conditions are met: + * + * 1. The code and/or derivative works are used only for private test networks consisting of no more than 10 P2P nodes. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#include +#include +#include +#include + +namespace graphene { namespace chain { + +object_id_type bond_create_offer_evaluator::do_evaluate( const bond_create_offer_operation& op ) +{ + const auto& d = db(); + + const auto& creator_account = op.creator( d ); + const auto& base_asset = op.collateral_rate.base.asset_id( d ); + const auto& quote_asset = op.collateral_rate.quote.asset_id( d ); + + // TODO: Check asset authorizations and withdrawals + + const auto& amount_asset = (op.amount.asset_id == op.collateral_rate.base.asset_id) ? base_asset : quote_asset; + + FC_ASSERT( !base_asset.is_transfer_restricted() && !quote_asset.is_transfer_restricted() ); + + if( base_asset.options.whitelist_markets.size() ) + FC_ASSERT( base_asset.options.whitelist_markets.find( quote_asset.id ) != base_asset.options.whitelist_markets.end() ); + if( base_asset.options.blacklist_markets.size() ) + FC_ASSERT( base_asset.options.blacklist_markets.find( quote_asset.id ) == base_asset.options.blacklist_markets.end() ); + + FC_ASSERT( d.get_balance( creator_account, amount_asset ) >= op.amount ); + + return object_id_type(); +} + +object_id_type bond_create_offer_evaluator::do_apply( const bond_create_offer_operation& op ) +{ + db().adjust_balance( op.creator, -op.amount ); + db().adjust_core_in_orders( op.creator(db()), op.amount ); + + const auto& offer = db().create( [&]( bond_offer_object& obj ) + { + obj.offered_by_account = op.creator; + obj.offer_to_borrow = op.offer_to_borrow; + obj.amount = op.amount; + obj.min_match = op.min_match; + obj.collateral_rate = op.collateral_rate; + obj.min_loan_period_sec = op.min_loan_period_sec; + obj.loan_period_sec = op.loan_period_sec; + obj.interest_apr = op.interest_apr; + } ); + + return offer.id; +} + + +object_id_type bond_cancel_offer_evaluator::do_evaluate( const bond_cancel_offer_operation& op ) +{ + _offer = &op.offer_id(db()); + FC_ASSERT( op.creator == _offer->offered_by_account ); + FC_ASSERT( _offer->amount == op.refund ); + return object_id_type(); +} + +object_id_type bond_cancel_offer_evaluator::do_apply( const bond_cancel_offer_operation& op ) +{ + assert( _offer != nullptr ); + db().adjust_balance( op.creator, op.refund ); + db().adjust_core_in_orders( op.creator(db()), -op.refund ); + db().remove( *_offer ); + return object_id_type(); +} + +object_id_type bond_accept_offer_evaluator::do_evaluate( const bond_accept_offer_operation& op ) +{ try { + _offer = &op.offer_id(db()); + + if( _offer->offer_to_borrow ) + FC_ASSERT( op.amount_borrowed.amount >= _offer->min_match ); + else + FC_ASSERT( op.amount_collateral.amount >= _offer->min_match ); + + FC_ASSERT( (op.amount_borrowed / op.amount_collateral == _offer->collateral_rate) || + (op.amount_collateral / op.amount_borrowed == _offer->collateral_rate) ); + + return object_id_type(); +} FC_CAPTURE_AND_RETHROW((op)) } + +object_id_type bond_accept_offer_evaluator::do_apply( const bond_accept_offer_operation& op ) +{ try { + + if( op.claimer == op.lender ) + { + db().adjust_balance( op.lender, -op.amount_borrowed ); + } + else // claimer == borrower + { + db().adjust_balance( op.borrower, -op.amount_collateral ); + db().adjust_core_in_orders( op.borrower(db()), op.amount_collateral ); + } + db().adjust_balance( op.borrower, op.amount_borrowed ); + + const auto& bond = db().create( [&]( bond_object& obj ) + { + obj.borrowed = op.amount_borrowed; + obj.collateral = op.amount_collateral; + obj.borrower = op.borrower; + obj.lender = op.lender; + + auto head_time = db().get_dynamic_global_properties().time; + obj.interest_apr = _offer->interest_apr; + obj.start_date = head_time; + obj.due_date = head_time + fc::seconds( _offer->loan_period_sec ); + obj.earliest_payoff_date = head_time + fc::seconds( _offer->min_loan_period_sec ); + } ); + + if( _offer->offer_to_borrow && op.amount_borrowed < _offer->amount ) + { + db().modify( *_offer, [&]( bond_offer_object& offer ){ + offer.amount -= op.amount_borrowed; + }); + } + else if( !_offer->offer_to_borrow && op.amount_collateral < _offer->amount ) + { + db().modify( *_offer, [&]( bond_offer_object& offer ){ + offer.amount -= op.amount_collateral; + }); + } + else + { + db().remove( *_offer ); + } + return bond.id; +} FC_CAPTURE_AND_RETHROW((op)) } + + + +object_id_type bond_claim_collateral_evaluator::do_evaluate( const bond_claim_collateral_operation& op ) +{ + _bond = &op.bond_id(db()); + auto head_time = db().get_dynamic_global_properties().time; + FC_ASSERT( head_time > _bond->earliest_payoff_date ); + + + FC_ASSERT( op.collateral_claimed <= _bond->collateral ); + if( _bond->borrower == op.claimer ) + { + auto elapsed_time = head_time - _bond->start_date; + auto elapsed_days = 1 + elapsed_time.to_seconds() / (60*60*24); + + fc::uint128 tmp = _bond->borrowed.amount.value; + tmp *= elapsed_days; + tmp *= _bond->interest_apr; + tmp /= (365 * GRAPHENE_100_PERCENT); + FC_ASSERT( tmp < GRAPHENE_MAX_SHARE_SUPPLY ); + _interest_due = asset(tmp.to_uint64(), _bond->borrowed.asset_id); + + FC_ASSERT( _interest_due + _bond->borrowed <= op.payoff_amount ); + + auto total_debt = _interest_due + _bond->borrowed; + + fc::uint128 max_claim = _bond->collateral.amount.value; + max_claim *= op.payoff_amount.amount.value; + max_claim /= total_debt.amount.value; + + FC_ASSERT( op.collateral_claimed.amount.value == max_claim.to_uint64() ); + } + else + { + FC_ASSERT( _bond->lender == op.claimer ); + FC_ASSERT( head_time > _bond->due_date ); + FC_ASSERT( _bond->collateral == op.collateral_claimed ); + FC_ASSERT( op.payoff_amount == asset(0,_bond->borrowed.asset_id ) ); + } + return object_id_type(); +} + +object_id_type bond_claim_collateral_evaluator::do_apply( const bond_claim_collateral_operation& op ) +{ + assert( _bond != nullptr ); + + const account_object& claimer = op.claimer(db()); + + db().adjust_core_in_orders( _bond->borrower(db()), -op.collateral_claimed ); + + if( op.payoff_amount.amount > 0 ) + { + db().adjust_balance( claimer, -op.payoff_amount ); + db().adjust_balance( op.lender, op.payoff_amount ); + } + db().adjust_balance( claimer, op.collateral_claimed ); + + if( op.collateral_claimed == _bond->collateral ) + db().remove(*_bond); + else + db().modify( *_bond, [&]( bond_object& bond ){ + bond.borrowed -= op.payoff_amount + _interest_due; + bond.collateral -= op.collateral_claimed; + bond.start_date = db().get_dynamic_global_properties().time; + }); + + return object_id_type(); +} + +} } // graphene::chain diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index 57c3cd37..1e1d8fac 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ #include #include +#include #include #include #include @@ -87,6 +89,10 @@ void database::initialize_evaluators() register_evaluator(); register_evaluator(); register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); register_evaluator(); register_evaluator(); register_evaluator(); @@ -112,6 +118,8 @@ void database::initialize_indexes() add_index< primary_index >(); add_index< primary_index >(); add_index< primary_index >(); + add_index< primary_index >(); + add_index< primary_index >(); add_index< primary_index > >(); add_index< primary_index >(); diff --git a/libraries/chain/db_market.cpp b/libraries/chain/db_market.cpp index 22fb778d..3f035dbc 100644 --- a/libraries/chain/db_market.cpp +++ b/libraries/chain/db_market.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -32,6 +33,7 @@ namespace graphene { namespace chain { calculate the USD->CORE price and convert all USD balances to CORE at that price and subtract CORE from total - any fees accumulated by the issuer in the bitasset are forfeit / not redeemed - cancel all open orders with bitasset in it + - any bonds with the bitasset as collateral get converted to CORE as collateral - any bitassets that use this bitasset as collateral are immediately settled at their feed price - convert all balances in bitasset to CORE and subtract from total - any prediction markets with usd as the backing get converted to CORE as the backing @@ -109,6 +111,38 @@ void database::globally_settle_asset( const asset_object& mia, const price& sett // settle all balances asset total_mia_settled = mia.amount(0); + // convert collateral held in bonds + const auto& bond_idx = get_index_type().indices().get(); + auto bond_itr = bond_idx.find( bitasset.id ); + while( bond_itr != bond_idx.end() ) + { + if( bond_itr->collateral.asset_id == bitasset.id ) + { + auto settled_amount = bond_itr->collateral * settlement_price; + total_mia_settled += bond_itr->collateral; + collateral_gathered -= settled_amount; + modify( *bond_itr, [&]( bond_object& obj ) { + obj.collateral = settled_amount; + }); + } + else break; + } + + // cancel all bond offers holding the bitasset and refund the offer + const auto& bond_offer_idx = get_index_type().indices().get(); + auto bond_offer_itr = bond_offer_idx.find( bitasset.id ); + while( bond_offer_itr != bond_offer_idx.end() ) + { + if( bond_offer_itr->amount.asset_id == bitasset.id ) + { + adjust_balance( bond_offer_itr->offered_by_account, bond_offer_itr->amount ); + auto old_itr = bond_offer_itr; + bond_offer_itr++; + remove( *old_itr ); + } + else break; + } + const auto& index = get_index_type().indices().get(); auto range = index.equal_range(mia.get_id()); for( auto itr = range.first; itr != range.second; ++itr ) diff --git a/libraries/chain/include/graphene/chain/bond_evaluator.hpp b/libraries/chain/include/graphene/chain/bond_evaluator.hpp new file mode 100644 index 00000000..224c3527 --- /dev/null +++ b/libraries/chain/include/graphene/chain/bond_evaluator.hpp @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2015, Cryptonomex, Inc. + * All rights reserved. + * + * This source code is provided for evaluation in private test networks only, until September 8, 2015. After this date, this license expires and + * the code may not be used, modified or distributed for any purpose. Redistribution and use in source and binary forms, with or without modification, + * are permitted until September 8, 2015, provided that the following conditions are met: + * + * 1. The code and/or derivative works are used only for private test networks consisting of no more than 10 P2P nodes. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#pragma once + +#include + +namespace graphene { namespace chain { + +class bond_create_offer_evaluator : public evaluator +{ + public: + typedef bond_create_offer_operation operation_type; + + object_id_type do_evaluate( const bond_create_offer_operation& op ); + object_id_type do_apply( const bond_create_offer_operation& op ); +}; + +class bond_cancel_offer_evaluator : public evaluator +{ + public: + typedef bond_cancel_offer_operation operation_type; + + object_id_type do_evaluate( const bond_cancel_offer_operation& op ); + object_id_type do_apply( const bond_cancel_offer_operation& op ); + + const bond_offer_object* _offer = nullptr; +}; + +class bond_accept_offer_evaluator : public evaluator +{ + public: + typedef bond_accept_offer_operation operation_type; + + object_id_type do_evaluate( const bond_accept_offer_operation& op ); + object_id_type do_apply( const bond_accept_offer_operation& op ); + + const bond_offer_object* _offer = nullptr; + asset _fill_amount; +}; + +class bond_claim_collateral_evaluator : public evaluator +{ + public: + typedef bond_claim_collateral_operation operation_type; + + object_id_type do_evaluate( const bond_claim_collateral_operation& op ); + object_id_type do_apply( const bond_claim_collateral_operation& op ); + + const bond_object* _bond = nullptr; + asset _interest_due; +}; + +} } // graphene::chain diff --git a/libraries/chain/include/graphene/chain/bond_object.hpp b/libraries/chain/include/graphene/chain/bond_object.hpp new file mode 100644 index 00000000..3c005c28 --- /dev/null +++ b/libraries/chain/include/graphene/chain/bond_object.hpp @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2015, Cryptonomex, Inc. + * All rights reserved. + * + * This source code is provided for evaluation in private test networks only, until September 8, 2015. After this date, this license expires and + * the code may not be used, modified or distributed for any purpose. Redistribution and use in source and binary forms, with or without modification, + * are permitted until September 8, 2015, provided that the following conditions are met: + * + * 1. The code and/or derivative works are used only for private test networks consisting of no more than 10 P2P nodes. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#pragma once + +#pragma once +#include +#include +#include + +namespace graphene { namespace chain { + + /** + * @ingroup object + */ + class bond_object : public graphene::db::abstract_object + { + public: + static const uint8_t space_id = protocol_ids; + static const uint8_t type_id = bond_object_type; + + asset_id_type collateral_type()const { return collateral.asset_id; } + + account_id_type borrower; + account_id_type lender; + asset borrowed; + /** if collateral is the core asset, then voting rights belong to the borrower + * because the borrower is owner of the collateral until they default + */ + asset collateral; + uint16_t interest_apr = 0; + time_point_sec start_date; + /** after this date the lender can collect the collateral at will or let it float */ + time_point_sec due_date; + /** the loan cannot be paid off before this date */ + time_point_sec earliest_payoff_date; + }; + + /** + * @ingroup object + */ + class bond_offer_object : public graphene::db::abstract_object + { + public: + static const uint8_t space_id = protocol_ids; + static const uint8_t type_id = bond_offer_object_type; + + asset_id_type asset_type()const { return amount.asset_id; } + + account_id_type offered_by_account; + bool offer_to_borrow = false; // Offer to borrow if true, and offer to lend otherwise + asset amount; + share_type min_match; ///< asset type same as ammount.asset_id + price collateral_rate; + uint32_t min_loan_period_sec = 0; + uint32_t loan_period_sec = 0; + uint16_t interest_apr = 0; + }; + + struct by_borrower; + struct by_lender; + struct by_offerer; + struct by_collateral; /// needed for blackswan resolution + struct by_asset; /// needed for blackswan resolution + + typedef multi_index_container< + bond_object, + indexed_by< + hashed_unique< tag, member< object, object_id_type, &object::id > >, + ordered_non_unique< tag, member >, + ordered_non_unique< tag, member >, + hashed_non_unique< tag, const_mem_fun > + > + > bond_object_multi_index_type; + + typedef generic_index bond_index; + + /** + * Todo: consider adding index of tuple + * Todo: consider adding index of tuple + */ + typedef multi_index_container< + bond_offer_object, + indexed_by< + hashed_unique< tag, member< object, object_id_type, &object::id > >, + ordered_non_unique< tag, member >, + hashed_non_unique< tag, const_mem_fun > + > + > bond_offer_object_multi_index_type; + + typedef generic_index bond_offer_index; + +}} // graphene::chain + +FC_REFLECT_DERIVED( graphene::chain::bond_object, (graphene::db::object), + (borrower)(lender)(borrowed)(collateral)(interest_apr)(start_date)(due_date)(earliest_payoff_date) ) +FC_REFLECT_DERIVED( graphene::chain::bond_offer_object, (graphene::db::object), (offered_by_account)(offer_to_borrow)(amount)(min_match)(collateral_rate)(min_loan_period_sec)(loan_period_sec)(interest_apr) ) diff --git a/libraries/chain/include/graphene/chain/operations.hpp b/libraries/chain/include/graphene/chain/operations.hpp index b670be1d..5b3cf85b 100644 --- a/libraries/chain/include/graphene/chain/operations.hpp +++ b/libraries/chain/include/graphene/chain/operations.hpp @@ -1248,6 +1248,123 @@ namespace graphene { namespace chain { } }; + /** + * @ingroup operations + * + * Bond offers are objects that exist on the blockchain and can be + * filled in full or in part by someone using the accept_bond_offer + * operation. When the offer is accepted a new bond_object is + * created that defines the terms of the loan. + * + * @return bond_offer_id + */ + struct bond_create_offer_operation + { + asset fee; + account_id_type creator; + bool offer_to_borrow = false; ///< Offer to borrow if true, and offer to lend otherwise + asset amount; ///< Amount to lend or secure depending on above + share_type min_match; ///< asset id same as amount.asset_id and sets the minimum match that will be accepted + price collateral_rate; ///< To derive amount of collateral or principle based on above + /** after this time the lender can let the loan float or collect the collateral at will */ + uint32_t min_loan_period_sec = 0; ///< the earliest the loan may be paid off + uint32_t loan_period_sec = 0; + uint16_t interest_apr = 0; ///< MAX_INTEREST_APR == 100% and is max value + + account_id_type fee_payer()const { return creator; } + void get_required_auth(flat_set& active_auth_set, flat_set&)const; + void validate()const; + share_type calculate_fee( const fee_schedule_type& k )const; + void get_balance_delta( balance_accumulator& acc, const operation_result& result = asset())const + { + acc.adjust( fee_payer(), -fee ); + acc.adjust( creator, -amount ); + } + }; + + /** + * @ingroup operations + * Subtracts refund from bond_offer.amount and frees bond_offer if refund == bond_offer.amount + */ + struct bond_cancel_offer_operation + { + asset fee; + account_id_type creator; + bond_offer_id_type offer_id; + asset refund; + + account_id_type fee_payer()const { return creator; } + void get_required_auth(flat_set& active_auth_set, flat_set&)const; + void validate()const; + share_type calculate_fee( const fee_schedule_type& k )const; + void get_balance_delta( balance_accumulator& acc, const operation_result& result = asset())const + { + acc.adjust( fee_payer(), -fee ); + acc.adjust( creator, refund ); + } + }; + + /** + * @ingroup operations + * @return new bond_id + */ + struct bond_accept_offer_operation + { + asset fee; + account_id_type claimer; + account_id_type lender; + account_id_type borrower; ///< included in case of offer to borrow, because borrower will receive funds + bond_offer_id_type offer_id; + asset amount_borrowed; ///< should equal amount_collateral * offer_id->collateral_rate + asset amount_collateral; ///< should equal amount_borrowed * offer_id->collateral_rate + + account_id_type fee_payer()const { return claimer; } + void get_required_auth(flat_set& active_auth_set, flat_set&)const; + void validate()const; + share_type calculate_fee( const fee_schedule_type& k )const; + void get_balance_delta( balance_accumulator& acc, const operation_result& result = asset())const + { + acc.adjust( fee_payer(), -fee ); + if( claimer == lender ) + acc.adjust( claimer, -amount_borrowed ); + else // claimer == borrower + acc.adjust( claimer, -amount_collateral ); + acc.adjust( borrower, amount_borrowed ); + } + }; + + /** + * @ingroup operations + * After the loan period the lender can claim + * the collateral, prior to the loan period expiring + * the borrower can claim it by paying off the loan + */ + struct bond_claim_collateral_operation + { + asset fee; + account_id_type claimer; ///< must be bond_id->lender or bond_id->borrower + account_id_type lender; ///< must be bond_id->lender + bond_id_type bond_id; + asset payoff_amount; + + /** the borrower can claim a percentage of the collateral propotional to the + * percentage of the debt+interest that was paid off + */ + asset collateral_claimed; + + account_id_type fee_payer()const { return claimer; } + void get_required_auth(flat_set& active_auth_set, flat_set&)const; + void validate()const; + share_type calculate_fee( const fee_schedule_type& k )const; + void get_balance_delta( balance_accumulator& acc, const operation_result& result = asset())const + { + acc.adjust( fee_payer(), -fee ); + acc.adjust( claimer, -payoff_amount ); + acc.adjust( claimer, collateral_claimed ); + acc.adjust( lender, payoff_amount ); + } + }; + /** * @brief Create a vesting balance. * @ingroup operations @@ -1427,6 +1544,10 @@ namespace graphene { namespace chain { global_parameters_update_operation, vesting_balance_create_operation, vesting_balance_withdraw_operation, + bond_create_offer_operation, + bond_cancel_offer_operation, + bond_accept_offer_operation, + bond_claim_collateral_operation, worker_create_operation, custom_operation > operation; @@ -1655,6 +1776,11 @@ 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::bond_create_offer_operation, (fee)(creator)(offer_to_borrow)(amount)(min_match)(collateral_rate)(min_loan_period_sec)(loan_period_sec)(interest_apr) ) +FC_REFLECT( graphene::chain::bond_cancel_offer_operation, (fee)(creator)(offer_id)(refund) ) +FC_REFLECT( graphene::chain::bond_accept_offer_operation, (fee)(claimer)(lender)(borrower)(offer_id)(amount_borrowed)(amount_collateral) ) +FC_REFLECT( graphene::chain::bond_claim_collateral_operation, (fee)(claimer)(lender)(bond_id)(payoff_amount)(collateral_claimed) ) + FC_REFLECT( graphene::chain::vesting_balance_create_operation, (fee)(creator)(owner)(amount)(vesting_seconds) ) FC_REFLECT( graphene::chain::vesting_balance_withdraw_operation, (fee)(vesting_balance)(owner)(amount) ) diff --git a/libraries/chain/include/graphene/chain/types.hpp b/libraries/chain/include/graphene/chain/types.hpp index 7a7d15d1..92c00b3a 100644 --- a/libraries/chain/include/graphene/chain/types.hpp +++ b/libraries/chain/include/graphene/chain/types.hpp @@ -114,6 +114,8 @@ namespace graphene { namespace chain { proposal_object_type, operation_history_object_type, withdraw_permission_object_type, + bond_offer_object_type, + bond_object_type, vesting_balance_object_type, worker_object_type, OBJECT_TYPE_COUNT ///< Sentry value which contains the number of different object types @@ -158,6 +160,8 @@ namespace graphene { namespace chain { class proposal_object; class operation_history_object; class withdraw_permission_object; + class bond_object; + class bond_offer_object; class vesting_balance_object; class witness_schedule_object; class worker_object; @@ -175,6 +179,8 @@ namespace graphene { namespace chain { typedef object_id< protocol_ids, proposal_object_type, proposal_object> proposal_id_type; typedef object_id< protocol_ids, operation_history_object_type, operation_history_object> operation_history_id_type; typedef object_id< protocol_ids, withdraw_permission_object_type,withdraw_permission_object> withdraw_permission_id_type; + typedef object_id< protocol_ids, bond_offer_object_type, bond_offer_object> bond_offer_id_type; + typedef object_id< protocol_ids, bond_object_type, bond_object> bond_id_type; typedef object_id< protocol_ids, vesting_balance_object_type, vesting_balance_object> vesting_balance_id_type; typedef object_id< protocol_ids, worker_object_type, worker_object> worker_id_type; @@ -373,6 +379,10 @@ namespace graphene { namespace chain { uint32_t membership_annual_fee; ///< the annual cost of a membership subscription uint32_t membership_lifetime_fee; ///< the cost to upgrade to a lifetime member uint32_t withdraw_permission_update_fee; ///< the cost to create/update a withdraw permission + uint32_t create_bond_offer_fee; + uint32_t cancel_bond_offer_fee; + uint32_t accept_bond_offer_fee; + uint32_t claim_bond_collateral_fee; uint32_t vesting_balance_create_fee; uint32_t vesting_balance_withdraw_fee; uint32_t global_settle_fee; @@ -493,6 +503,8 @@ FC_REFLECT_ENUM( graphene::chain::object_type, (proposal_object_type) (operation_history_object_type) (withdraw_permission_object_type) + (bond_offer_object_type) + (bond_object_type) (vesting_balance_object_type) (worker_object_type) (OBJECT_TYPE_COUNT) @@ -547,6 +559,10 @@ FC_REFLECT( graphene::chain::fee_schedule_type, (membership_annual_fee) (membership_lifetime_fee) (withdraw_permission_update_fee) + (create_bond_offer_fee) + (cancel_bond_offer_fee) + (accept_bond_offer_fee) + (claim_bond_collateral_fee) (vesting_balance_create_fee) (vesting_balance_withdraw_fee) (global_settle_fee) @@ -593,6 +609,8 @@ FC_REFLECT_TYPENAME( graphene::chain::custom_id_type ) FC_REFLECT_TYPENAME( graphene::chain::proposal_id_type ) FC_REFLECT_TYPENAME( graphene::chain::operation_history_id_type ) FC_REFLECT_TYPENAME( graphene::chain::withdraw_permission_id_type ) +FC_REFLECT_TYPENAME( graphene::chain::bond_offer_id_type ) +FC_REFLECT_TYPENAME( graphene::chain::bond_id_type ) FC_REFLECT_TYPENAME( graphene::chain::vesting_balance_id_type ) FC_REFLECT_TYPENAME( graphene::chain::worker_id_type ) FC_REFLECT_TYPENAME( graphene::chain::relative_key_id_type ) diff --git a/libraries/chain/operations.cpp b/libraries/chain/operations.cpp index c08027e2..86073a71 100644 --- a/libraries/chain/operations.cpp +++ b/libraries/chain/operations.cpp @@ -804,6 +804,77 @@ share_type custom_operation::calculate_fee( const fee_schedule_type& k )const return (data.size() * k.data_fee)/1024; } +void bond_create_offer_operation::get_required_auth( flat_set& active_auth_set, flat_set& )const +{ + active_auth_set.insert( creator ); +} + +void bond_create_offer_operation::validate()const +{ try { + FC_ASSERT( fee.amount >= 0 ); + FC_ASSERT( amount.amount > 0 ); + collateral_rate.validate(); + FC_ASSERT( (amount * collateral_rate).amount > 0 ); + FC_ASSERT( min_loan_period_sec > 0 ); + FC_ASSERT( loan_period_sec >= min_loan_period_sec ); + FC_ASSERT( interest_apr <= GRAPHENE_MAX_INTEREST_APR ); +} FC_CAPTURE_AND_RETHROW((*this)) } + +share_type bond_create_offer_operation::calculate_fee( const fee_schedule_type& schedule )const +{ + return schedule.create_bond_offer_fee; +} + + +void bond_cancel_offer_operation::get_required_auth(flat_set& active_auth_set, flat_set&)const +{ + active_auth_set.insert( creator ); +} +void bond_cancel_offer_operation::validate()const +{ + FC_ASSERT( fee.amount > 0 ); + FC_ASSERT( refund.amount > 0 ); +} +share_type bond_cancel_offer_operation::calculate_fee( const fee_schedule_type& k )const +{ + return k.cancel_bond_offer_fee; +} + +void bond_accept_offer_operation::get_required_auth(flat_set& active_auth_set, flat_set&)const +{ + active_auth_set.insert( claimer ); +} + +void bond_accept_offer_operation::validate()const +{ + FC_ASSERT( fee.amount > 0 ); + (amount_collateral / amount_borrowed).validate(); + FC_ASSERT( claimer == borrower || claimer == lender ); + FC_ASSERT( borrower != lender ); +} + +share_type bond_accept_offer_operation::calculate_fee( const fee_schedule_type& k )const +{ + return k.accept_bond_offer_fee; +} +void bond_claim_collateral_operation::get_required_auth(flat_set& active_auth_set, flat_set&)const +{ + active_auth_set.insert( claimer ); +} + +void bond_claim_collateral_operation::validate()const +{ + FC_ASSERT( fee.amount > 0 ); + FC_ASSERT(payoff_amount.amount >= 0 ); + FC_ASSERT(collateral_claimed.amount >= 0 ); + FC_ASSERT( payoff_amount.asset_id != collateral_claimed.asset_id ); +} + +share_type bond_claim_collateral_operation::calculate_fee( const fee_schedule_type& k )const +{ + return k.claim_bond_collateral_fee; +} + void worker_create_operation::get_required_auth(flat_set& active_auth_set, flat_set&) const { active_auth_set.insert(owner); diff --git a/libraries/plugins/account_history/account_history_plugin.cpp b/libraries/plugins/account_history/account_history_plugin.cpp index d8e1148e..77badee4 100644 --- a/libraries/plugins/account_history/account_history_plugin.cpp +++ b/libraries/plugins/account_history/account_history_plugin.cpp @@ -232,6 +232,18 @@ struct operation_get_impacted_accounts _impacted.insert( account_id_type() ); } + void operator()( const bond_create_offer_operation& o )const { } + void operator()( const bond_cancel_offer_operation& o )const { } + void operator()( const bond_accept_offer_operation& o )const { + _impacted.insert( o.borrower ); + _impacted.insert( o.lender ); + } + void operator()( const bond_claim_collateral_operation& o )const + { + _impacted.insert( o.lender ); + _impacted.insert( o.claimer ); + } + void operator()( const vesting_balance_create_operation& o )const { _impacted.insert( o.creator ); diff --git a/libraries/wallet/cache.cpp b/libraries/wallet/cache.cpp index d5c3f382..3c68e708 100644 --- a/libraries/wallet/cache.cpp +++ b/libraries/wallet/cache.cpp @@ -26,6 +26,7 @@ #include #include #include +#include using namespace fc; using namespace graphene::chain; @@ -86,6 +87,10 @@ object* create_object( const variant& v ) return create_object_of_type< operation_history_object >( v ); case withdraw_permission_object_type: return create_object_of_type< withdraw_permission_object >( v ); + case bond_offer_object_type: + return create_object_of_type< bond_offer_object >( v ); + case bond_object_type: + return create_object_of_type< bond_object >( v ); default: ; } diff --git a/programs/js_operation_serializer/main.cpp b/programs/js_operation_serializer/main.cpp index 21db9e66..deefe0a1 100644 --- a/programs/js_operation_serializer/main.cpp +++ b/programs/js_operation_serializer/main.cpp @@ -16,6 +16,7 @@ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include +#include #include #include #include diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index 27d57445..1717a04a 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -150,6 +151,12 @@ void database_fixture::verify_asset_supplies( )const { total_balances[asset_id_type()] += witness_obj.accumulated_income; } + for( const bond_offer_object& bond_offer : db.get_index_type().indices() ) + { + total_balances[ bond_offer.amount.asset_id ] += bond_offer.amount.amount; + if( bond_offer.amount.asset_id == asset_id_type() ) + core_in_orders += bond_offer.amount.amount; + } for( const vesting_balance_object& vbo : db.get_index_type< simple_index >() ) total_balances[ vbo.balance.asset_id ] += vbo.balance.amount; diff --git a/tests/tests/operation_tests.cpp b/tests/tests/operation_tests.cpp index e3bc8c00..27d4e5d2 100644 --- a/tests/tests/operation_tests.cpp +++ b/tests/tests/operation_tests.cpp @@ -2080,6 +2080,7 @@ BOOST_AUTO_TEST_CASE( margin_call_black_swan ) * 2) Short Orders for BitAsset backed by BitUSD * 3) Call Orders for BitAsset backed by BitUSD * 4) Issuer Fees + * 5) Bond Market Collateral * * This test should fail until the black swan handling code can * perform a recursive blackswan for any other BitAssets that use @@ -2137,6 +2138,69 @@ BOOST_AUTO_TEST_CASE( unimp_transfer_cashback_test ) } } +BOOST_AUTO_TEST_CASE( bond_create_offer_test ) +{ try { + bond_create_offer_operation op; + op.fee = asset( 0, 0 ); + op.creator = account_id_type(); + op.amount = asset( 1, 0 ); + op.collateral_rate = price( asset( 1, 0 ), asset( 1, 1 ) ); + op.min_loan_period_sec = 1; + op.loan_period_sec = 1; + + // Fee must be non-negative + REQUIRE_OP_VALIDATION_SUCCESS( op, fee, asset( 1, 0 ) ); + REQUIRE_OP_VALIDATION_SUCCESS( op, fee, asset( 0, 0 ) ); + REQUIRE_OP_VALIDATION_FAILURE( op, fee, asset( -1, 0 ) ); + + // Amount must be positive + REQUIRE_OP_VALIDATION_SUCCESS( op, amount, asset( 1, 0 ) ); + REQUIRE_OP_VALIDATION_FAILURE( op, amount, asset( 0, 0 ) ); + REQUIRE_OP_VALIDATION_FAILURE( op, amount, asset( -1, 0 ) ); + + // Collateral rate must be valid + REQUIRE_OP_VALIDATION_SUCCESS( op, collateral_rate, price( asset( 1, 0 ), asset( 1, 1 ) ) ); + REQUIRE_OP_VALIDATION_FAILURE( op, collateral_rate, price( asset( 0, 0 ), asset( 1, 1 ) ) ); + REQUIRE_OP_VALIDATION_FAILURE( op, collateral_rate, price( asset( 1, 0 ), asset( 0, 1 ) ) ); + REQUIRE_OP_VALIDATION_FAILURE( op, collateral_rate, price( asset( 1, 0 ), asset( 1, 0 ) ) ); + + // Min loan period must be at least 1 sec + REQUIRE_OP_VALIDATION_SUCCESS( op, min_loan_period_sec, 1 ); + REQUIRE_OP_VALIDATION_FAILURE( op, min_loan_period_sec, 0 ); + + // Loan period must be greater than min load period + REQUIRE_OP_VALIDATION_SUCCESS( op, loan_period_sec, op.min_loan_period_sec + 1 ); + REQUIRE_OP_VALIDATION_FAILURE( op, loan_period_sec, 0 ); + + // Interest APR cannot be greater than max + REQUIRE_OP_VALIDATION_FAILURE( op, interest_apr, GRAPHENE_MAX_INTEREST_APR + 1 ); + REQUIRE_OP_VALIDATION_SUCCESS( op, interest_apr, GRAPHENE_MAX_INTEREST_APR ); + REQUIRE_OP_VALIDATION_SUCCESS( op, interest_apr, 0 ); + + // Setup world state we will need to test actual evaluation + INVOKE( create_uia ); + const auto& test_asset = get_asset( "TEST" ); + const auto& nathan_account = create_account( "nathan" ); + transfer( account_id_type()( db ), nathan_account, asset( 1, 0 ) ); + + op.creator = nathan_account.get_id(); + op.collateral_rate.quote.asset_id = test_asset.get_id(); + trx.operations.emplace_back( op ); + + // Insufficient funds in creator account + REQUIRE_THROW_WITH_VALUE( op, creator, account_id_type( 1 ) ); + + // Insufficient principle + REQUIRE_THROW_WITH_VALUE( op, amount, asset( 2, 0 ) ); + + // Insufficient collateral + op.offer_to_borrow = true; + REQUIRE_THROW_WITH_VALUE( op, amount, asset( 1, test_asset.get_id() ) ); + + // This op should be fully valid + REQUIRE_OP_EVALUATION_SUCCESS( op, offer_to_borrow, false ); +} FC_LOG_AND_RETHROW() } + BOOST_AUTO_TEST_CASE( vesting_balance_create_test ) { try { INVOKE( create_uia );