From b4ea9ec194aa7834760623af5f8166a791300727 Mon Sep 17 00:00:00 2001 From: Daniel Larimer Date: Sun, 11 Mar 2018 22:08:47 -0400 Subject: [PATCH] progress on margin --- contracts/eosiolib/asset.hpp | 21 +++ contracts/eosiolib/datastream.hpp | 15 ++- contracts/exchange/exchange.hpp | 199 +++++++++++++++++++++++++++- tests/wasm_tests/exchange_tests.cpp | 13 +- 4 files changed, 244 insertions(+), 4 deletions(-) diff --git a/contracts/eosiolib/asset.hpp b/contracts/eosiolib/asset.hpp index 73c19c779..ca9524cda 100644 --- a/contracts/eosiolib/asset.hpp +++ b/contracts/eosiolib/asset.hpp @@ -185,6 +185,27 @@ namespace eosio { return result; } + friend extended_asset operator - ( const extended_asset& a, const extended_asset& b ) { + eosio_assert( a.symbol == b.symbol, "type mismatch" ); + eosio_assert( a.contract == b.contract, "type mismatch" ); + extended_asset result( asset(a.amount - b.amount, a.symbol), a.contract ); + + if( b.amount > 0 ) + eosio_assert( a.amount > result.amount, "underflow" ); + if( b.amount < 0 ) + eosio_assert( a.amount < result.amount, "overflow" ); + + return result; + } + + friend extended_asset operator + ( const extended_asset& a, const extended_asset& b ) { + eosio_assert( a.symbol == b.symbol, "type mismatch" ); + eosio_assert( a.contract == b.contract, "type mismatch" ); + extended_asset result( asset(a.amount + b.amount, a.symbol), a.contract ); + + return result; + } + EOSLIB_SERIALIZE( extended_asset, (amount)(symbol)(contract) ) }; diff --git a/contracts/eosiolib/datastream.hpp b/contracts/eosiolib/datastream.hpp index 8fb38f7bb..1a84a1d5d 100644 --- a/contracts/eosiolib/datastream.hpp +++ b/contracts/eosiolib/datastream.hpp @@ -291,10 +291,11 @@ inline datastream& operator>>(datastream& ds, int64_t& d) { * @param d value to serialize */ template -inline datastream& operator<<(datastream& ds, const uint64_t d) { +inline datastream& operator<<(datastream& ds, const uint64_t& d) { ds.write( (const char*)&d, sizeof(d) ); return ds; } + /** * Deserialize a uint64_t from a stream * @brief Deserialize a uint64_t @@ -307,6 +308,18 @@ inline datastream& operator>>(datastream& ds, uint64_t& d) { return ds; } +template +inline datastream& operator<<(datastream& ds, const double& d) { + ds.write( (const char*)&d, sizeof(d) ); + return ds; +} + +template +inline datastream& operator>>(datastream& ds, double& d) { + ds.read((char*)&d, sizeof(d) ); + return ds; +} + /** * Serialize a int16_t into a stream * @brief Serialize a int16_t diff --git a/contracts/exchange/exchange.hpp b/contracts/exchange/exchange.hpp index 7fa13d7ec..1c5991f2b 100644 --- a/contracts/exchange/exchange.hpp +++ b/contracts/exchange/exchange.hpp @@ -7,6 +7,17 @@ namespace eosio { typedef double real_type; using boost::container::flat_map; + struct margin_state { + extended_asset total_lendable; + extended_asset total_lent; + double least_collateralized = 0; + + extended_asset interest_pool; + double interest_shares = 0; + + EOSLIB_SERIALIZE( margin_state, (total_lendable)(total_lent)(least_collateralized)(interest_pool)(interest_shares) ) + }; + struct exchange_state { account_name manager; extended_asset supply; @@ -16,7 +27,9 @@ namespace eosio { extended_asset balance; uint32_t weight = 500; - EOSLIB_SERIALIZE( connector, (balance)(weight) ) + margin_state peer_margin; /// peer_connector collateral lending balance + + EOSLIB_SERIALIZE( connector, (balance)(weight)(peer_margin) ) }; connector base; @@ -120,6 +133,28 @@ namespace eosio { currency _excurrencies; public: + /** + * We calculate a unique scope for each market/borrowed_symbol/collateral_symbol and then + * instantiate a table of margin positions... with in this table each user has exactly + * one position and therefore the owner can serve as the primary key. + */ + struct margin_position { + account_name owner; + extended_asset borrowed; + extended_asset collateral; + double call_price = 0; + + uint64_t get_call()const { return uint64_t(call_price); } + uint64_t primary_key()const { return owner; } + + EOSLIB_SERIALIZE( margin_position, (owner)(borrowed)(collateral)(call_price) ) + }; + + typedef eosio::multi_index > + > margins; + + exchange( account_name self ) :_this_contract(self),_excurrencies(self){} @@ -158,6 +193,9 @@ namespace eosio { EOSLIB_SERIALIZE( withdraw, (from)(quantity) ) }; + + + void on( const withdraw& w ) { require_auth( w.from ); eosio_assert( w.quantity.amount >= 0, "cannot withdraw negative balance" ); @@ -165,6 +203,124 @@ namespace eosio { currency::inline_transfer( _this_contract, w.from, w.quantity, "withdraw" ); } + struct upmargin { + symbol_type market; + account_name borrower; + extended_asset delta_borrow; + extended_asset delta_collateral; + + EOSLIB_SERIALIZE( upmargin, (market)(borrower)(delta_borrow)(delta_collateral) ) + }; + + void margin_call( exchange_state& state, exchange_state::connector& c, margins& marginstable ) { + auto price_idx = marginstable.get_index(); + auto pos = price_idx.begin(); + if( pos == price_idx.end() ) + return; + + auto receipt = convert( state, pos->collateral, pos->borrowed.get_extended_symbol() ); + eosio_assert( receipt.amount >= pos->borrowed.amount, "programmer error: insufficient collateral to cover" );/// VERY BAD, SHOULD NOT HAPPEN + auto change_debt = receipt - pos->borrowed; + + auto change_collat = convert( state, change_debt, pos->collateral.get_extended_symbol() ); + + adjust_balance( pos->owner, change_collat ); + + c.peer_margin.total_lent.amount -= pos->borrowed.amount; + price_idx.erase(pos); + + pos = price_idx.begin(); + if( pos != price_idx.end() ) + c.peer_margin.least_collateralized = pos->call_price; + else + c.peer_margin.least_collateralized = double(uint64_t(-1)); + } + + void on( const upmargin& b ) { + require_auth( b.borrower ); + auto marketid = b.market.name(); + + margins base_margins( _this_contract, marketid ); + margins quote_margins( _this_contract, marketid << 1 ); + + markets market_table( _this_contract, marketid ); + auto market_state = market_table.find( marketid ); + eosio_assert( market_state != market_table.end(), "unknown market" ); + + auto state = *market_state; + + eosio_assert( b.delta_borrow.amount != 0 || b.delta_collateral.amount != 0, "no effect" ); + eosio_assert( b.delta_borrow.get_extended_symbol() != b.delta_collateral.get_extended_symbol(), "invalid args" ); + eosio_assert( state.base.balance.get_extended_symbol() == b.delta_borrow.get_extended_symbol() || + state.quote.balance.get_extended_symbol() == b.delta_borrow.get_extended_symbol(), + "invalid asset for market" ); + eosio_assert( state.base.balance.get_extended_symbol() == b.delta_collateral.get_extended_symbol() || + state.quote.balance.get_extended_symbol() == b.delta_collateral.get_extended_symbol(), + "invalid asset for market" ); + + + auto adjust_margin = [&b]( exchange_state::connector& c, margins& mtable ) { + margin_position temp_pos; + auto existing = mtable.find( b.borrower ); + if( existing == mtable.end() ) { + eosio_assert( b.delta_borrow.amount > 0, "borrow neg" ); + eosio_assert( b.delta_collateral.amount > 0, "collat neg" ); + temp_pos.owner = b.borrower; + temp_pos.borrowed = b.delta_borrow; + temp_pos.collateral = b.delta_collateral; + temp_pos.call_price = double( temp_pos.collateral.amount ) / temp_pos.borrowed.amount; + } else { + temp_pos = *existing; + temp_pos.borrowed += b.delta_borrow; + temp_pos.collateral += b.delta_borrow; + eosio_assert( temp_pos.borrowed.amount > 0, "neg borrowed" ); + eosio_assert( temp_pos.collateral.amount > 0, "neg collateral" ); + if( temp_pos.borrowed.amount > 0 ) + temp_pos.call_price = double( temp_pos.collateral.amount ) / temp_pos.borrowed.amount; + else + temp_pos.call_price = double( uint64_t(-1) ); + } + c.peer_margin.total_lent += b.delta_borrow; + + auto least = mtable.begin(); + if( least == existing ) ++least; + + if( least != mtable.end() ) + c.peer_margin.least_collateralized = least->call_price; + if( temp_pos.call_price < c.peer_margin.least_collateralized ) + c.peer_margin.least_collateralized = temp_pos.call_price; + }; + + + auto temp_state = state; + + margins* mt = nullptr; + auto baseptr = &exchange_state::base; + auto quoteptr = &exchange_state::quote; + auto conptr = quoteptr; + + if( b.delta_borrow.get_extended_symbol() == state.base.balance.get_extended_symbol() ) { + mt = &base_margins; + conptr = baseptr; + } else { + mt = "e_margins; + } + + adjust_margin( temp_state.*conptr, *mt ); + while( requires_margin_call( temp_state ) ) { + // margin_call( state ); + temp_state = state; + adjust_margin( temp_state.*conptr, *mt ); + } + adjust_margin( state.*conptr, *mt ); + + /// if this succeeds then the borrower will see their balances adjusted accordingly, + /// if they don't have sufficient balance to either fund the collateral or pay off the + /// debt then this will fail before we go further. + adjust_balance( b.borrower, b.delta_borrow, "borrowed" ); + adjust_balance( b.borrower, -b.delta_collateral, "collateral" ); + } + struct trade { account_name seller; symbol_type market; @@ -176,6 +332,20 @@ namespace eosio { EOSLIB_SERIALIZE( trade, (seller)(market)(sell)(min_receive)(expire)(fill_or_kill) ) }; + bool requires_margin_call( const exchange_state& state, const exchange_state::connector& con ) { + if( con.peer_margin.total_lent.amount > 0 ) { + auto tmp = state; + auto base_total_col = int64_t(con.peer_margin.total_lent.amount * con.peer_margin.least_collateralized); + auto covered = convert( tmp, extended_asset( base_total_col, con.balance.get_extended_symbol()), con.peer_margin.total_lent.get_extended_symbol() ); + if( covered.amount <= con.peer_margin.total_lent.amount ) + return true; + } + return false; + } + bool requires_margin_call( const exchange_state& state ) { + return requires_margin_call( state, state.base ) || requires_margin_call( state, state.quote ); + } + extended_asset convert( exchange_state& state, extended_asset from, extended_symbol to ) { auto sell_symbol = from.get_extended_symbol(); auto ex_symbol = extended_symbol( state.supply.symbol, _this_contract ); @@ -217,8 +387,22 @@ namespace eosio { eosio_assert( market_state != market_table.end(), "unknown market" ); auto state = *market_state; + auto temp = state; + auto output = convert( temp, t.sell, t.min_receive.get_extended_symbol() ); + + margins base_margins( _this_contract, marketid ); + margins quote_margins( _this_contract, marketid << 1 ); + while( requires_margin_call( temp ) ) { + if( t.sell.get_extended_symbol() == state.base.balance.get_extended_symbol() ) { + margin_call( state, state.quote, quote_margins ); + } else { + margin_call( state, state.base, base_margins ); + } + temp = state; + output = convert( temp, t.sell, t.min_receive.get_extended_symbol() ); + } + state = temp; - auto output = convert( state, t.sell, t.min_receive.get_extended_symbol() ); print( name(t.seller), " ", t.sell, " => ", output, "\n" ); if( t.min_receive.amount != 0 ) { @@ -259,6 +443,7 @@ namespace eosio { typedef eosio::multi_index exaccounts; + /** * Keep a cache of all accounts tables we access */ @@ -309,6 +494,16 @@ namespace eosio { s.supply = extended_asset(c.initial_supply, _this_contract); s.base.balance = c.base_deposit; s.quote.balance = c.quote_deposit; + + s.base.peer_margin.total_lent.symbol = c.base_deposit.symbol; + s.base.peer_margin.total_lent.contract = c.base_deposit.contract; + s.base.peer_margin.total_lendable.symbol = c.base_deposit.symbol; + s.base.peer_margin.total_lendable.contract = c.base_deposit.contract; + + s.quote.peer_margin.total_lent.symbol = c.quote_deposit.symbol; + s.quote.peer_margin.total_lent.contract = c.quote_deposit.contract; + s.quote.peer_margin.total_lendable.symbol = c.quote_deposit.symbol; + s.quote.peer_margin.total_lendable.contract = c.quote_deposit.contract; }); _excurrencies.create_currency( { .issuer = _this_contract, diff --git a/tests/wasm_tests/exchange_tests.cpp b/tests/wasm_tests/exchange_tests.cpp index 107131acb..9f4e75008 100644 --- a/tests/wasm_tests/exchange_tests.cpp +++ b/tests/wasm_tests/exchange_tests.cpp @@ -24,6 +24,16 @@ using namespace eosio::chain::contracts; using namespace eosio::testing; using namespace fc; +struct margin_state { + extended_asset total_lendable; + extended_asset total_lent; + double least_collateralized = 0; + + extended_asset interest_pool; + double interest_shares = 0; +}; +FC_REFLECT( margin_state, (total_lendable)(total_lent)(least_collateralized)(interest_pool)(interest_shares) ) + struct exchange_state { account_name manager; extended_asset supply; @@ -32,13 +42,14 @@ struct exchange_state { struct connector { extended_asset balance; uint32_t weight = 500; + margin_state peer_margin; }; connector base; connector quote; }; -FC_REFLECT( exchange_state::connector, (balance)(weight) ); +FC_REFLECT( exchange_state::connector, (balance)(weight)(peer_margin) ); FC_REFLECT( exchange_state, (manager)(supply)(fee)(base)(quote) ); class exchange_tester : public tester { -- GitLab