1. 程式人生 > >【劉文彬】【精解】EOS標準貨幣體系與原始碼實現分析

【劉文彬】【精解】EOS標準貨幣體系與原始碼實現分析

原文連結:醒者呆的部落格園,https://www.cnblogs.com/Evsward/p/eos-exchange.html

EOS智慧合約中包含一個exchange合約,它支援使用者建立一筆交易,是任何兩個基本貨幣型別之間的交易。這個合約的作用是跨不同幣種(都是EOS上的標準貨幣型別)的,通過各自與EOS主鏈價值進行錨定,然後再相互發起交易兌換。要搞清楚的是,這與區塊鏈“傳統的”交易所並不一樣,那個主要是集中在交易撮合上面,而且必須是同一幣種。

關鍵字:EOS token 經濟模型,exchange,Pegged Currency,LTV,cmake,跨token交易,ubuntu編譯boost庫,通證模型,抵押資產,token價值轉換

標準貨幣體系

上面一直提到標準貨幣(standard currency),在EOS.io中也針對這一標準貨幣的設計給出了官方文件,本章先來弄清楚標準貨幣的概念,然後再繼續分析exchange。

文章的名字叫《Pegged Derivative Currency Design》

Pegged Currency的含義是什麼?

Currency pegging是用來固定匯率的一種方式,通過匹配當前貨幣與其他貨幣的價值錨定,或者其他價值度量方式,例如黃金和白銀。所以Pegged Currency可以理解為匯率穩定的貨幣。

所以,這篇文章的題目可以翻譯為:一種價值穩定的衍生貨幣設計。

目前市面已有的Pegged Derivative 貨幣,例如BitUSD,位元美元,它是一種以加密貨幣作為抵押。發行人是短線持有美元而額外長地持有加密貨幣。而購買者是單純地長期持有美元。

原始設計

位元股BitShares創造了第一個可行的價值穩定的資產系統:

允許任何人獲得一個最小的抵押狀況是,公佈抵押物和獲得的BitUSD的價值比率在最小的1.5:1(抵押物:負債率)。

最少的抵押狀況下,要強迫BitUSD持有人在任何市場價格下跌超過美元幾個百分點以下的時候的流動性(如果BitUSD持有人選擇使用強制清算是不允許的)。換句話說,就是在當前貨幣下跌的時候,也要保證貨幣流通性,這是為了貨幣狀況健康運營而考慮。

為了防止價格補償(直接通過增發和賣出來控制價格)的濫用,所有的強制清算會被推遲。當發生“黑天鵝”事件(極小可能性發生,但實際上卻發生了)的時候,所有的短線會通過價格補償擁有他們自己的清算狀況,而所有的BitUSD的持有者只能承諾一個固定的贖回率(清算時固定一個贖回率)。

這種設計的問題體現在:

  • 在BitUSD/BitShares市場體系廣泛傳播下,其實流通性是很低的。
  • 短線持股人會承擔所有風險,只有在BitShares上漲時才會賺取利潤。
  • 黑天鵝(BlackSwans)總會出現,並且伴隨巨大破壞力。
  • 這是一個人人為己的一個模型。
  • 由於風險收益率,供給會被限制。
  • 抵押物的苛刻要求限制了槓桿leverage。

新的想法

這是一種讓短線持股人通過提供一種高流通性的價值穩定的資產來穩定貨幣資產。他們會通過鼓勵人們交易他們的穩值資產來獲益,賺取交易費而不是在投機市場尋求高槓杆。也會通過賺取短線持股的利息。

實施步驟

一個初始使用者儲存了一個可擔保貨幣(C)到一個智慧合約,並且提供一個初始化價格補償。通過C:D等於1.5:1的價格補償率發行一個新的負債token(D),這個token會被儲存在Bancor市場製造商。

Bancor是為以太坊的代幣的價值判斷以及流通機制。通過智慧合約,可將這些代幣作為儲備金,讓任何人隨時隨地通過智慧合約進行快速兌換,銷燬代幣,提高代幣流通性。

這樣一來,因為沒有D token被賣出,所以市場製造商是0槓桿。那個初始使用者也會收到交換token(E)從市場製造商那裡。

我們繼續,人們現在可以購買E或者D token,Bancor演算法會提供基於C,E,D之間的流通性。由於市場製造商的收費,E的價值將隨著C而增長。

C = 智慧代幣或者母貨幣儲備
D或E = 獎勵代幣(發放給初期持有人,以及社群貢獻者)
抵押率C:D = 價值C(抵押物)借款D(負債)比(反過來就是LTV,loan to value)

價值維穩

我們做了這麼多工作,其實目的就是要保障D這種token(token本身就是衍生貨幣)是符合Pegged Currency的設定。最直接的指標就是與美元價值(C就可以是這個角色)的錨定浮動範圍,要儘可能小,這個範圍的浮動要小到讓更多的人(套匯者)願意持有和交易D token。下面有幾種可能性:

  • D的價值超過美元5%
    • 抵押物價值(原值)增加,C:D>1.5,這時候要發行更多的D token,來降低比率到1.5
    • 原值降低,C:D<1.5,調整token體量(減少市面流通量)以降低贖回價格(持有人不願賠錢硬拋)來降低D token的價值到與美元一致。

市場體量 = 聯結者體量(Bancor)
贖回價格:在到期之前,發行人可以回購持有者的token。

  • D的價值少於美元5%
    • 原值增加,C:D>1.5,調整token體量擡高贖回價格(持有人願意被贖回),從而提高市面上token的價值,最終趕上美元。
    • 原值降低,C:D<1.5,這個比較複雜,因為token的價格要低於美元,同時它的原值也低於負債,說明這個token已經真的價值降低了。那麼就需要增資補償
      • 中止其他token,例如E到C和E到D的交易。
      • 在C到E和D到E的交易中間提供獎金(用來補償)。
      • 在D到E的轉化上會在迴圈外收到D,而不是加到市場製造商那裡。
      • 在C與D相互轉化上不做任何改變。
      • 中止嘗試調整製造商比率來防衛價格補償,使價格上漲至高於美元1%(這是比較健康的)

exchange

基於上面標準貨幣體系,我們可以看到在EOS上面的token的經濟模型,這是一個很有競爭力的模型,可以保證每個token的價值穩定,而不是狂漲狂跌,真正使EOS上的經濟生態健康穩定運轉起來。下面來研究exchange智慧合約的主要功能。

CMakeLists

首先來看CMake設定,上文《【精解】EOS智慧合約演練》中也有CMake的應用,但並沒有搞太清楚,這裡在討論exchange的CMakeLists配置之前,我們先來搞定cmake本身。

cmake

CMake於C++ 類似maven於java的存在,它可以用來對軟體進行構建、測試以及打包等工作。我們在研究C++ 專案的時候,CMake是很好的構建工具,一定要熟悉掌握。

正如maven的配置檔案主要是通過pom.xml一樣,CMake的工作是通過CMakeLists.txt檔案來描述的。所以掌握CMakeLists.txt檔案的配置方法是必要的。

  • cmake_minimum_required,這個配置位於第一行,指定了專案構建所需的CMake的最低版本。
  • project(banner),括號內填寫當前專案名。
  • set(MY_VAR “hello”),CMake可以通過set關鍵字來設定文字變數。(相當於全域性變數)
  • set (OTHER_VAR " M Y V A R w o r l d ! &quot; ) {MY_VAR} world!&quot;),可以通過“ {}”引用上面定義過的變數內容。

我們在IDE中,也可以像直接通過專案中的pom檔案匯入maven專案那樣,通過專案中的CMakelists.txt檔案匯入CMake專案。

像以上這種設定命令有很多,我們可以參照《CMake官方文件:命令解釋》來查閱相關命令的含義以及使用。

exchange cmake

file(GLOB ABI_FILES "*.abi")
add_wast_executable(TARGET exchange
  INCLUDE_FOLDERS "${STANDARD_INCLUDE_FOLDERS}"
  LIBRARIES libc++ libc eosiolib
  DESTINATION_FOLDER ${CMAKE_CURRENT_BINARY_DIR}
)
configure_file("${ABI_FILES}" "${CMAKE_CURRENT_BINARY_DIR}" COPYONLY)
add_dependencies( exchange currency )

add_executable(test_exchange test_exchange.cpp )
#bfp/lib/pack.c bfp/lib/posit.cpp bfp/lib/util.c bfp/lib/op2.c)
target_link_libraries( test_exchange fc )
target_include_directories( test_exchange PUBLIC fixed_point/include )
  • file是檔案操作命令,它的引數:
    • GLOB,通過檔案匹配找到檔案列表然後將他們存入變數中。
    • ABI_FILES,變數名,會將匹配到的檔案內容儲存在該變數中。
    • ".abi",globbing expressions,檔名匹配表示式。例如 “.vt?”,“f[3-5].txt”
  • add_wast_executable,cmake的自定義module。

我們在原始碼位置eos/CMakeModules中可以找到wasm.cmake檔案,進去以後可以發現

macro(add_wast_executable)

自定義module也是以巨集(命令集對外為單一命令)的形式(Excel中我之前寫過巨集指令碼,也是同一個詞macro)。這一段add_wast_executable內容很多,我就不貼上了,主要內容是為了wasm環境構建程式碼,包括對打包內容的描述,對狀態的判斷處理等各種命令的集合,其中又包含了很多module巨集。

  • configure_file,複製一個檔案到另一個位置,並修改其內容。COPYONLY只複製不修改。
  • add_executable,增加依賴。
  • add_executable,新增一個可執行的專案使用指定的原始檔。
  • target_link_libraries,link一個庫到target(target是第一個引數)。
  • target_include_directories,include目錄新增到target。

exchange.abi

abi檔案是通過eosiocpp工具通過exchange.cpp生成的,具體可參照《EOS智慧合約演練》。

exchange_accounts

從這裡開始我們來詳細分析exchange合約的原始碼內容,exchange.cpp需要引用exchange_accounts, exchange_state以及market_state這三個庫。其中market_state又依賴兩外兩個庫,因此我們先從比較獨立的exchange_accounts入手。

exchange_accounts.hpp

#pragma once
#include <eosiolib/asset.hpp>
#include <eosiolib/multi_index.hpp>

namespace eosio {

   using boost::container::flat_map;// 相當於java中的import,下面可以直接使用flat_map方法。

   /**
    *  每個使用者都有他們自己的賬戶,並且這個賬戶是帶有exchange合約的。這可以讓他們保持跟蹤一個使用者是如何抵押各種擴充套件資產型別的。假定儲存一個獨立的flat_map,包含一個特定使用者的所有餘額,這個使用者比起打破使用擴充套件標識來排序的多重索引表,將更加實際的
    */
   struct exaccount {
      account_name                         owner;// uint64_t型別,64位無符號整型
      flat_map<extended_symbol, int64_t>   balances;// extended_symbol是asset.hpp中定義的

      uint64_t primary_key() const { return owner; }// 結構體包含一個primary_key方法是不可變的,const,實現也有了,直接是返回owner。
      EOSLIB_SERIALIZE( exaccount, (owner)(balances) )// EOSLIB_SERIALIZE這是一種自定義的模板,是一種反射機制,可以給結構體賦值,第一個引數為結構體名字,後面的引數用括號分別括起來,傳入當前兩個成員變數。
   };

   typedef eosio::multi_index<N(exaccounts), exaccount> exaccounts;// multi_index是一個類,這行定義了一個變數exaccounts,它的型別是一種多重索引。


   /**
    *  提供一個抽象介面,為使用者的餘額排序。這個類快取了資料表,以提供高效地多重訪問。
    */
   struct exchange_accounts {
      exchange_accounts( account_name code ):_this_contract(code){}// 給私有成員賦值

      void adjust_balance( account_name owner, extended_asset delta, const string& reason = string() );//調整餘額,傳入owner、擴充套件資產,reason。exchange\_accounts.cpp會實現該函式。 

      private: 
         account_name _this_contract;// 私有成員 \_this\_contract
         /**
          *  保留一個快取,用來儲存我們訪問的所有使用者表
          */
         flat_map<account_name, exaccounts> exaccounts_cache;// flat_map型別的快取exaccounts_cache,儲存的是賬戶名和以上結構體exaccounts。
   };
} /// namespace eosio

multi_index這裡再簡單介紹一下。它的模板定義是

template<uint64_t TableName, typename T, typename... Indices>

泛型中第一個引數是表名,第二個是多重索引。

N函式的原始碼:

 /**
    * @brief 用來生成一個編譯時間,它是64位無符號整型。傳入的引數X是一個base32編碼的字串的解釋。
    * @ingroup types
    */
   #define N(X) ::eosio::string_to_name(#X)

可參考文章《EOS技術研究:合約與資料庫互動》,下面來看一下exchange_accounts.cpp原始碼:

#include <exchange/exchange_accounts.hpp>

namespace eosio {

   void exchange_accounts::adjust_balance( account_name owner, extended_asset delta, const string& reason ) {
      (void)reason;// reason當做一個備註,不可修改的。

      auto table = exaccounts_cache.find( owner );//通過account\_name查詢到對應的exaccount結構體物件資料。
      if( table == exaccounts_cache.end() ) {// 如果這個資料是最後一個,則將當前資料重新包裝放入exaccounts_cache,同時將exaccounts_cache第一位的資料重新賦值給table
         table = exaccounts_cache.emplace( owner, exaccounts(_this_contract, owner )  ).first;
      }
      auto useraccounts = table->second.find( owner );//table現在有值了,在table下一個位置查詢owner
      if( useraccounts == table->second.end() ) {// 如果這個使用者是table下一個位置的結尾資料,則將owner重新組裝資料放入table
         table->second.emplace( owner, [&]( auto& exa ){
           exa.owner = owner;
           exa.balances[delta.get_extended_symbol()] = delta.amount;
           eosio_assert( delta.amount >= 0, "overdrawn balance 1" );//斷言,當extended_assert資產的數目小於0時,列印日誌:透支餘額1
         });
      } else {// 如果該使用者不是table下一個位置的結尾資料,則修改以該使用者為key的資料,
         table->second.modify( useraccounts, 0, [&]( auto& exa ) {
           const auto& b = exa.balances[delta.get_extended_symbol()] += delta.amount;// 擴充套件標識的餘額加上extended_assert資產的數目為b
           eosio_assert( b >= 0, "overdrawn balance 2" );// 斷言,當b小於0時,列印日誌:透支餘額2
         });
      }
   }

} /// namespace eosio

它實現了adjust_balance函式。這個函式主要實現了對賬戶資料的管理,餘額的判斷與處理。

exchange_state

exchange_state庫的原始碼我就不張貼了,這裡進行一個總結:

  • exchange_state.hpp,標頭檔案中主要聲明瞭一些變數結構體,
    • 包括邊緣狀態margin_state,返回的是一個extended_asset
    • interest_shares,所有的給那些借出人分配的共享空間,當某人未借款,他們可以獲得total_lendable * user_interest_shares / interest_shares。當付過利息以後,會顯示在變數total_lendable。
    • exchange_state結構體是使用bancor數學建立一個在兩種資產型別中的50/50的中繼。這個bancor的狀態,exchange是完全包含在這個結構體中。這個API沒有額外的影響和使用。
  • exchange_state.cpp,原始檔中主要實現了標頭檔案中這幾個結構體中的一些函式,包括
    • convert_to_exchange,通過傳入一種extended_asset資產,將它轉換成exchange token,相當於token在原有發行量的基礎上,按照新的extended_asset資產抵押發行了新的token。
    • convert_from_exchange,通過傳入一定量的exchange token(注意exchange token也是extended_asset資產型別),將其轉化為其他extended_asset資產,相當於回購了部分token,降低了token保有量。
    • convert,傳入一個extended_asset資產引數,以及一個extended_symbol引數,通過判斷symbol的種類,呼叫以上convert_to_exchange或convert_from_exchange函式進行轉換處理,最終將傳入的extended_asset資產轉換為攜帶extended_symbol。
    • requires_margin_call,傳入一個connector,connector在以上轉換函式中都作為引數並且在轉換過程中發生了作用,這裡是對connector引數進行判斷,是否需要呼叫邊緣處理(即與值peer_margin.total_lent.amount作比較)

下面是connector的原始碼部分:

struct connector {
 extended_asset balance;// 餘額
 uint32_t       weight = 500;// 權重

 margin_state   peer_margin; /// peer_connector 抵押借貸餘額,margin_state型別

 EOSLIB_SERIALIZE( connector, (balance)(weight)(peer_margin) )還是那個初始化工具。
};

exchange_state庫中最重要的函式就是上面這幾個轉換函式,掌握這些函式都能幹哪些事,未來我們可以直接測試呼叫或者在其他原始碼中出現繼續分析。

market_state

這是基於以上exchange_accounts以及exchange_state兩個庫的庫,它的內容也很多,不適宜全部粘貼出來。

  • market_state.hpp,該標頭檔案中包含了結構體
    • margin_position,我們針對每一個market/borrowed_symbol/collateral_symbol型別的資料計算了一個唯一的作用域,然後例舉了一個邊緣位置表,通過這個表,每個使用者可以明確地擁有一個位置,因此owner可以作為主鍵。
    • loan_position,借貸位置。
    • market_state(C++ 語法補充:結構體中也可以有private成員,這跟類很相似了其實)。與邊緣位置或者限制單數一起維護了一個狀態
  • market_state.cpp,原始檔中實現了很多函式。這些函式實現了市場借貸關係,餘額數量等操作處理,具體我們在exchange主庫中通過具體業務進行介紹。

exchange

這是整個exchange合約的主庫(通常我會將一個名字的標頭檔案加原始檔合併稱為一個庫,這也是C++ 的命名習慣)。

exchange.hpp

標頭檔案,主要聲明瞭一個類exchange,這裡麵包含了三個私有成員,以及七個公有函式,還有三個公有結構體,下面貼一下原始碼吧:

#include <eosiolib/types.hpp>
#include <eosiolib/currency.hpp>
#include <boost/container/flat_map.hpp>
#include <cmath>
#include <exchange/market_state.hpp>

namespace eosio {

   /**
    *  這個合約可以讓使用者在任意一對標準貨幣型別之間建立一個exchange,這個exchange是基於一個在購買方和發行方雙邊的價值等額的條件下而建立的。為了預防舍入誤差,初始化金額應該包含大量的base以及quote貨幣的數量,並且exchange 共享應該在最大初始化金額的100倍的數量。使用者在他們通過exchange交易前,必須先存入資金到exchange。每次一個exchange建立一個新的貨幣時,相應的交易市場製造商也會被建立。貨幣供應以及貨幣符號必須是唯一的並且它使用currency合約的表來管理。
    */
   class exchange {
      private:
         account_name      _this_contract;// 私有,賬戶名
         currency          _excurrencies;// 貨幣
         exchange_accounts _accounts;// exchange的賬戶

      public:
         exchange( account_name self )
         :_this_contract(self),
          _excurrencies(self),
          _accounts(self)
         {}
         // 建立
         void createx( account_name    creator,
                       asset           initial_supply,
                       uint32_t        fee,
                       extended_asset  base_deposit,
                       extended_asset  quote_deposit
                     );
         // 訂金
         void deposit( account_name from, extended_asset quantity );
         // 提現
         void withdraw( account_name  from, extended_asset quantity );
         // 借出
         void lend( account_name lender, symbol_type market, extended_asset quantity );

         // 不借?
         void unlend(
            account_name     lender,
            symbol_type      market,
            double           interest_shares,
            extended_symbol  interest_symbol
         );

         // 邊緣覆蓋結構體
         struct covermargin {
            account_name     borrower;
            symbol_type      market;
            extended_asset   cover_amount;
         };
    
         // 上側邊緣
         struct upmargin {
            account_name     borrower;
            symbol_type      market;
            extended_asset   delta_borrow;
            extended_asset   delta_collateral;
         };
         // 交易結構體
         struct trade {
            account_name    seller;
            symbol_type     market;
            extended_asset  sell;
            extended_asset  min_receive;
            uint32_t        expire = 0;
            uint8_t         fill_or_kill = true;
         };

         // 函式名根據引數列表方法過載,在xxx上執行exchange
         void on( const trade& t    );
         void on( const upmargin& b );
         void on( const covermargin& b );
         void on( const currency::transfer& t, account_name code );

         // 應用
         void apply( account_name contract, account_name act );
   };
} // namespace eosio

exchange.cpp

該原始檔中實現了以上標頭檔案中定義的所有公有方法。

測試

先定義兩個標準貨幣base和quote,他們都是exchange_state型別:

exchange_state state;
state.supply = 100000000000ll;// 發行量
//state.base.weight  = state.total_weight / 2.;
state.base.balance.amount = 100000000;
state.base.balance.symbol = "USD";
state.base.weight = .49;
//state.quote.weight = state.total_weight / 2.;
state.quote.balance.amount = state.base.balance.amount;
state.quote.balance.symbol = "BTC";
state.quote.weight = .51;

print_state( state );
插曲:ubuntu編譯boost庫

首先在boost官網下載最新庫檔案,目前我下載的版本是boost_1_67_0.tar.bz2。

  • 下載好壓縮包,解壓縮tar --bzip2 -xf boost_1_67_0.tar.bz2
  • 解壓後的資料夾轉移到自己的習慣位置管理好,然後進入該目錄
  • 先執行./booststrap.sh進行boost庫編譯。
  • 再執行sudo ./b2 install進行命令安裝。

然後,我們再開啟CLion,CMake自動編譯專案eos,會發現console中已經顯式編譯成功的字樣。

接下來繼續我們的測試。直接run 主函式,首先打印出來的是"USD"和"BTC"的發行資訊,

-----------------------------
supply: 1e+11
base: 1e+08 USD
quote: 1e+08 BTC

-----------------------------

可以看到,這與程式碼中定義的總髮行量以及包含的兩種符號型別的token的各自發行量,都是準確的。

自定義數字資產型別

exchange_state是在測試類中我們自定義的數字資產型別,下面是它的結構:

struct exchange_state {
   token_type  supply;// 發行量
   symbol_type symbol = exchange_symbol;// exchange符號

   // 兩個聯結器base和quote
   connector  base;
   connector  quote;
   // 交易
   void transfer( account_name user, asset q ) {
      output[balance_key{user,q.symbol}] += q.amount;
   }
   map<balance_key, token_type> output; 
   vector<margin>               margins;
};

exchange_state數字資產中,包含一個總髮行量,兩個成員資產base和quote,他們是connector型別,這個型別也是自定義的(與上面介紹的原始碼稍有不同,稍後在測試完成以後會總結他們的區別),交易函式以及一個自定義集合output和margins,下面來看connector的定義:

struct connector {
   asset      balance; // asset資產型別
   real_type  weight = 0.5;
   token_type total_lent; /// 發行商從使用者的貸款
   token_type total_borrowed; /// 發行商借給使用者
   token_type total_available_to_lend; /// 可借出的有效數量
   token_type interest_pool; /// 利息池,是所獲得的總利息,但不一定每個使用者都可以申請使用

   // 以下三個方法都在本檔案下被實現了。
   void  borrow( exchange_state& ex, const asset& amount_to_borrow ); 
   asset convert_to_exchange( exchange_state& ex, const asset& input );
   asset convert_from_exchange( exchange_state& ex, const asset& input );
};

這個connector有一個餘額,一個權重(可理解為佔有exchange_state數字資產的比例),它的一些銀行資產功能屬性,貸款拆借利息等,以及connector本身作為資產可以與其他exchange_state數字資產進行轉換,拆借等功能。餘額成員是asset資產型別,這個型別也是一個自定義結構體:

struct asset {
   token_type amount;
   symbol_type symbol;
};

它具備一個總數量和符號兩個成員。所以以上我們給exchange_state數字資產定義了兩個connector,“BTC”和“USD”以及它們各自的發行量,正是採用這個asset的結構進行賦值的。

打印出state內容以後,顯示的是兩種token"USD"和"BTC"的發行資訊,接下來,我們利用exchange中的一些函式功能進行兩種token之間的轉換及交易。

auto new_state = convert(state, "dan", asset{100, "USD"}, asset{0, "BTC"});
print_state(new_state);

看一下這裡面的convert函式的宣告:

/**
 *  通過給出的一個當前state,計算出一個新的state返回。
 */
exchange_state convert( const exchange_state& current,// 當前state
                        account_name user,// 使用者
                        asset        input,// 輸入資產
                        asset        min_output,// 最小輸出資產
                        asset*       out = nullptr) {

所以我們來解讀第一行convert程式碼的意思為:

一個名為“dan”的使用者,現有資產狀態為上面已列印的state,輸入資產為100個USD,最小輸出資產為0個BTC(注意輸入資產和最小輸出資產必須是不同的,否則無法轉化)。

下面看輸出print_\state結果:

-----------------------------
supply: 1e+11
base: 1e+08 USD
quote: 9.99999e+07 BTC
dan  96.0783 BTC
dan  0 EXC
dan  -100 USD

結果解讀:

  • supply和base的數量都沒變
  • quote的數量少了100個BTC(0.00001e+07)
  • dan的BTC多出來96.0783個。
  • dan的EXC為0(本次交易中沒有涉及到,EXC是預設token符號)
  • dan的USD少了100個。

重新解讀這一行convert程式碼的意思為:

state數字資產(我們最早設定的),dan根據state資產的格式拿出來自己賬戶中的100個USD(dan本身沒有USD,所以是欠款狀態)作為抵押想exchange BTC,,而BTC是quote(base和quote也可以理解為使用者)的符號,所以quote的數量少了相應的100個BTC。最後,要將這100個BTC打入dan的賬戶裡面,而為什麼程式設計了96.0783個而不是100個呢?

除錯

上面我們將問題拋了出來,下面我們對程式碼進行debug,來分析這100個BTC在發放給使用者的時候是如何轉變的?我們打個斷點,開始執行程式,走到convert函式中,由於我們的USD等於base的符號,所以執行到了convert_to_exchange函式。

asset connector::convert_to_exchange(exchange_state &ex, const asset &input) {

    real_type R(ex.supply);// 1e+11
    real_type S(balance.amount + input.amount); //100000100,等於state資產得新發行100個USD
    real_type F(weight);//0.489999999999999991118,USD所佔比重,state初始化時已經設定好
    real_type T(input.amount);//100
    real_type ONE(1.0);//1

    auto E = R * (ONE - std::pow(ONE + T / S, F));// 根據這個演算法得到對應的state資產的增髮量的值。pow是cmath庫的一個函式,有兩個引數,返回結果為第一個引數為底,第二個引數為冪值的結果。
    // (1e+11)*(1-(1+100/100000100)^+0.489999999999999991118),這得藉助計算器了,算出結果為:-48999.9385,約等於程式執行結果-48999.938505084501827。

    token_type issued = -E; //48999.9385,增發100個USD,實際上要增發state這麼多。

    ex.supply += issued;// 更新總髮行量,加入以上計算的值。
    balance.amount += input.amount;//state的USD connector(可理解為基於某穩值數字資產下的token)的餘額可以增加100個USD了。

    return asset{issued, exchange_symbol};// 最後是以EXC資產增發48999.9385個的形式返回。
}
EXC是state的“原值”符號,USD和BTC是基於EXC抵押資產發行的數字token。

繼續除錯,回到convert函式中。我們獲得了要對應增發EXC的數量,那麼要具體執行影響到state數字資產,是通過:

result.output[balance_key{user, final_output.symbol}] += final_output.amount;// 將增發EXC的數量新增至state的output集合中。

output存放形式:

  • 集合中一個元素位置,下標為0開始儲存。
  • 每個元素是一個pair型別。不懂C++ 中pair型別的可以參考《C++ 語法》。可以理解為一個元組,包含一對值first和second。
  • first是一個自定義結構體balance_key,包含一個賬戶名稱成員和一個符號成員。這裡對應的是"dan",“EXC”。
  • second是一個增髮量48999.9385。

結果就是EXC總賬戶通過dan增發了48999.9385,然後接下來繼續,

result.output[balance_key{user, input.symbol}] -= input.amount;

這是給dan賬戶進行減持,同樣的,我們列出output的存放形式:

  • 下標1
  • pair型別,first,second
  • first是"dan",“USD”
  • second是一個銷燬量100個。

結果就是dan個人賬戶欠了100個USD,dan在呼叫convert的時候,要求最小輸出資產是BTC型別的,而現在針對輸入資產型別USD以及EXC相應的操作已經做完。下面要做的是EXC和BTC的convert。

if (min_output.symbol != final_output.symbol) {// 當計算的最終輸出資產的符號與傳入的最小輸出資產不一致時,要呼叫本身convert來轉換。
    return convert(result, user, final_output, min_output, out);
}

攜帶新的引數EXC和BTC再次進入convert函式時,state數字資產已經發生了變化,它的總髮行量變為100000048999.93851,base的USD的餘額變為100000100,quote的BTC的餘額不變,仍舊為1億。我們新帶過來的引數是:

  • 48999.938505084501個EXC作為輸入資產
  • 最小輸出資產仍舊為第一次呼叫convert的0個BTC

由於我們這一次處理的輸入資產型別就是state的預設符號EXC,所以會走另外一個處理分支,根據最小輸出資產型別會執行convert_from_exchange函式:

initial_output = result.quote.convert_from_exchange(result, initial_output);

convert_from_exchange函式原始碼:

asset connector::convert_from_exchange(exchange_state &ex, const asset &input) {

    real_type R(ex.supply - input.amount);// 先找回原值:1e+11
    real_type S(balance.amount);// BTC餘額不變,仍為1億個1e+8
    real_type F(weight);// 權重為0.51
    real_type E(input.amount);// EXC的輸入數量48999.93851
    real_type ONE(1.0);

    real_type T = S * (std::pow(ONE + E / R, ONE / F) - ONE);// 1e+8*((1+48999.93851/1e+11)^(1/0.51)-1),通過科學計算器了,算出結果為:96.07833342,約等於程式執行結果96.0783334103356735645。
    // 這是通過抵押資產EXC的增髮量來反推對應的BTC的增髮量。

    auto out = T;

    ex.supply -= input.amount;// 將EXC增發的部分減掉,其實是維持了原有增髮量1e+11不變。
    balance.amount -= token_type(out);// BTC的總量減少了96.07833342(這部分發給dan了),變為99999903.921666592。
    return asset{token_type(out), balance.symbol};//最終以BTC減掉(發放出去)96.07833342的形式返回。
}

它的函式體與上面的convert_to_exchange函式很相似,但細一看會發現裡面的某些數值運算髮生了變化。然後,我們繼續回到二重convert函式中,BTC發給dan的部分(實際上從dan的角度上來講,可以是BTC增發)具體執行為:

result.output[balance_key{user, final_output.symbol}] += final_output.amount;// 將發給dan的96.07833342加到dan的賬戶裡。

結果就是dan賬戶中多了96.07833342個BTC。然後對作為輸入資產的EXC進行處理:

result.output[balance_key{user, input.symbol}] -= input.amount;

結果就是EXC總賬戶通過dan減持掉48999.9385。此時,由於上面的convert_from_exchange函式返回的是BTC的資產,與原始最小輸出資產型別相同,所以不必要再次進入一個convert巢狀。直接返回state,包含以上四個加粗資訊,這裡再重新列出來:

  1. EXC總賬戶通過dan增發了48999.9385
  2. dan個人賬戶欠了100個USD
  3. dan賬戶中多了96.07833342個BTC
  4. EXC總賬戶通過dan減持掉48999.9385

1和4互相抵消,等於state的總髮行量不變,仍舊為原始的1e+11。所以state中會新增賬戶dan的資訊,它的USD和BTC以及EXC(中間涉及到了中轉交易,EXC相當於一箇中間價值錨定,用來建立兩種token交易的通道)。最終達到了與程式輸出相等的結果:

-----------------------------
supply: 1e+11
base: 1e+08 USD
quote: 9.99999e+07 BTC
dan  96.0783 BTC
dan  0 EXC
dan  -100 USD

-----------------------------

總結

我們通過一個簡單的測試完成了對exchange合約的學習,exchange合約教會我們可以通過EOS建立自己的生態模型,通證模型,我們可以錨定抵押資產,發行token,通過權重的形式發行多個token等等,非常靈活,這與本篇文章前半部分所描述的那種價值穩定的數字貨幣的設計是吻合的。在測試程式中,我們簡單實現了exchange原始碼中的convert函式,各種自定義結構體,例如connector,exchange_state等等,基本上所有測試檔案中的函式與結構都可以在exchange原始碼中找到。我們在上面原始碼分析的過程中還比較混沌,但通過測試檔案的梳理,再回頭去看上面的原始碼分析,會有新的體會。原始碼中各種結構以及函式是更加精密與強壯的,但是測試檔案和exchange原始碼相同的是:他們的通證模型是相同的。我們通過測試和原始碼更加充分理解了EOS的靈活的通證模型。有任何問題,歡迎來討論。

參考資料

  • EOS原始碼

相關文章和視訊推薦

圓方圓學院彙集大批區塊鏈名師,打造精品的區塊鏈技術課程。 在各大平臺都長期有優質免費公開課,歡迎報名收看。

公開課地址:https://ke.qq.com/course/345101