作者:慢霧安全團隊
公眾號:https://mp.weixin.qq.com/s/epaClx3Z08cEUGy6CpuvNA

系列閱讀:EOS REX 系列之從源碼開始玩轉 REX(一)

前言

上一篇文章粗略分析了整個買賣 rex 的流程,由于篇幅的原因,剩下有一些細節沒有分析到位。所以,這篇文章將在上一篇文章的基礎上對一些細節進行深入的分析。

前情回顧

上一篇介紹了買賣 rex 的流程,涉及到了幾個函數,我們一起回顧下:

1、 deposit:用于充值,將EOS變成SEOS,也叫預備金。
2、 withdraw:用于提現,將SEOS換回EOS
3、 buyrex:用于從用戶的預備金中扣除相應的份額,并用于rex的購買。
4、 sellrex:用于賣出已經結束鎖定的rex,并將本金連帶收益一起放進用戶的預備金賬戶中。
5、 add_to_rex_pool:用于將用戶購買的rex放進rex_pool中,并根據 rex_pool 中的相關信息計算出用戶能夠購買的rex的數量,被buyrex函數調用。
6、 fill_rex_order:處理用戶賣單,計算收益。

以上幾個函數除了 sell_rexfill_rex_order 其他函數都介紹得差不多了,本文將重點介紹這兩個函數的細節。

sellrex 函數

void system_contract::sellrex( const name& from, const asset& rex )
   {
      require_auth( from );

      runrex(2);

      auto bitr = _rexbalance.require_find( from.value, "user must first buyrex" );
      check( rex.amount > 0 && rex.symbol == bitr->rex_balance.symbol,
             "asset must be a positive amount of (REX, 4)" );
      process_rex_maturities( bitr ); ///先收獲成熟的rex
      check( rex.amount <= bitr->matured_rex, "insufficient available rex" );///只能賣成熟的rex

      auto current_order = fill_rex_order( bitr, rex );///拿到出租EOS得到的分紅
      asset pending_sell_order = update_rex_account( from, current_order.proceeds, current_order.stake_change );
      //訂單狀態不成功
      if ( !current_order.success ) {
         /**
          * REX order couldn't be filled and is added to queue.
          * If account already has an open order, requested rex is added to existing order.
          */
         auto oitr = _rexorders.find( from.value );
         if ( oitr == _rexorders.end() ) {
            oitr = _rexorders.emplace( from, [&]( auto& order ) {
               order.owner         = from;
               order.rex_requested = rex;
               order.is_open       = true;
               order.proceeds      = asset( 0, core_symbol() );
               order.stake_change  = asset( 0, core_symbol() );
               order.order_time    = current_time_point();
            });
         } else {
            _rexorders.modify( oitr, same_payer, [&]( auto& order ) {
               order.rex_requested.amount += rex.amount;
            });
         }
         pending_sell_order.amount = oitr->rex_requested.amount; 
      }
      check( pending_sell_order.amount <= bitr->matured_rex, "insufficient funds for current and scheduled orders" );
      // dummy action added so that sell order proceeds show up in action trace
      if ( current_order.success ) {
         dispatch_inline( null_account, "sellresult"_n, { }, std::make_tuple( current_order.proceeds ) );
      }
   }

以上為sellrex函數的具體實現,從開頭開始一步一步進行分析。首先拋開runrex這個函數,這個函數并不屬于本次討論的范圍,runrex函數主要用于處理rex_pool的信息,包括處理到期的資源租賃訂單,回收用戶資源,處理用戶的rex賣單等,有興趣的同學可以先自行研究,以后的文章也會進行單獨的分析。

接上篇分析,sellrex函數我們分析到了fill_rex_order函數就沒有繼續往下分析了,fill_rex_order函數也只是講了最核心的收益公式,這次我們來仔細進行分析。sellrex流程如下:

1、 經過了一系列的檢查之后,獲取用于已經解鎖的rex的數量,調用 fill_rex_order獲取用戶的賣單。
2、 賣單攜帶著訂單的完成狀態,這是一個flag,分為成功和失敗兩種狀態,當狀態為失敗的時候,進入上文的if條件

2.1、訂單狀態成功

訂單狀態成功的時候current_order.proceed的值大于 0,這個時候通過 update_rex_account將賣rex的所得轉至用戶的儲備金賬戶。用戶就可以直接進行提現或者繼續下一輪的購買了。

2.2、訂單狀態為失敗

這個時候創建一個 order,我們這里為了不混淆,不說賣單,而是說為欠條,是一個 REX 平臺給你的借條。什么意思呢?打個比方,你去商店訂購商品,商品存貨不足,這時候怎么辦呢?這時候商店就會給你打一個單,這個單記錄了你是誰,你要買多少的商品,買的時間等信息,等有貨了就會根據這個單給你補上商品。REX 也是同樣的道理,用戶在賣rex的時候,由于rex_pool中的資金不足以支付用戶的本金 + 收益,就會將用戶的訂單暫時掛起。這就是 REX 給你打的欠條,當 REX 資金充足的時候,就會把錢還你。當sellrex失敗的時候,這個借條記錄了以下信息:

(1) 賣rex的用戶。
(2) 要賣的rex的數量(記錄在rex_requested字段中)。
(3) 用戶的收益,此時為 0,因為rex沒有賣出去,收益是不存在的。
(4) 抵押狀態,這個抵押狀態是由于buyrex的時候,根據購買的數量會產生的相應的票權。
(5) 這個欠條創建的時間。

3、 最后,檢查掛起的金額有沒有超過已經解鎖的 rex 的數量。

以上就把sellrex完整的講完了,但是還有一個疑問,就是為什么會存在資金不足的情況,以及如何判定資金不足?這些秘密都在fill_rex_order里面。下面就詳細的分析 fill_rex_order函數。

rex_order_outcome system_contract::fill_rex_order( const rex_balance_table::const_iterator& bitr, const asset& rex )
   {
      auto rexitr = _rexpool.begin();
      const int64_t S0 = rexitr->total_lendable.amount;
      const int64_t R0 = rexitr->total_rex.amount;
      const int64_t p  = (uint128_t(rex.amount) * S0) / R0; ///越多人借用資源收益越高
      const int64_t R1 = R0 - rex.amount; ///更新rex pool中rex的數量
      const int64_t S1 = S0 - p; ///更新rex pool中EOS的數量
      asset proceeds( p, core_symbol() ); ///獲得的收益
      asset stake_change( 0, core_symbol() );
      bool  success = false; ///默認訂單完成狀態為0

      check( proceeds.amount > 0, "proceeds are negligible" );

      const int64_t unlent_lower_bound = rexitr->total_lent.amount;
      //計算能未質押的rex pool中的EOS的數量,用于接下來觀察是否足夠支付用戶產生的rex利潤
      const int64_t available_unlent   = rexitr->total_unlent.amount - unlent_lower_bound; // available_unlent <= 0 is possible 
      //rexpool中的錢足夠支付rex利潤
      if ( proceeds.amount <= available_unlent ) {
         const int64_t init_vote_stake_amount = bitr->vote_stake.amount;
         const int64_t current_stake_value    = ( uint128_t(bitr->rex_balance.amount) * S0 ) / R0;
         _rexpool.modify( rexitr, same_payer, [&]( auto& rt ) {
            rt.total_rex.amount      = R1;///更新rex pool中的rex的數量
            rt.total_lendable.amount = S1; ///更新lenableEOS數量
            rt.total_unlent.amount   = rt.total_lendable.amount - rt.total_lent.amount; ///減少unlent數據
         });
         //對用戶的rexbalance賬戶進行操作
         _rexbalance.modify( bitr, same_payer, [&]( auto& rb ) {
            rb.vote_stake.amount   = current_stake_value - proceeds.amount;
            rb.rex_balance.amount -= rex.amount;
            rb.matured_rex        -= rex.amount; ///減少已經成熟的rex的數量
         });
         stake_change.amount = bitr->vote_stake.amount - init_vote_stake_amount;
         success = true;
      ///不夠錢支付的情況
      } else {
         proceeds.amount = 0;
      }

      return { success, proceeds, stake_change };
   }

上一篇文章我們分析了核心的收益公式是怎么計算出來的。這次從11行開始,看看這個函數做了什么:

1、 首先獲取unlent_lower_bound的值,即最低未出租rex_pool中的 EOS 的數量。這個字段等于用戶從rex_pool中借用資源的總量,是以 EOS 為單位的。

2、 計算available_unlent的值,這個值有可能為負數,為什么呢?假設一個場景,你是一個投資經理,你手上有很多投資人的錢,然后你把投資人的錢拿出去放貸收取利息,那么請問,這個時候你手上的資金還有多少?答案自然是:投資人的錢 - 放貸資金 + 放貸收益。REX 相當于這個投資經理,用戶可以用少量的成本(EOS)換取大量的貸款(資源),這個時候,REX 的資金池中的資金就就變成了:用戶的資金 - 租用的金額 + 租用收益。根據前面的描述,用于租用資源的資金總是小于 REX 平臺出租出去的資金,也就是說 在持續出租資源的時候,rex_pool中的資金總是不斷變少的(這里不討論系統收益的情況)。想清楚這一點,就能明白為什么available_unlent的值為負數了,當出租出去的資金大于rex_pool中當前資金 + 收益的時候,這個值就會為負數。

3、 判斷用戶出售的 rex 獲得的收益是否小于rex_pool中的剩余資金,相當于投資人想要回自己的錢,這個時候分兩種情況:
3.1、 如果資金不夠,那么這個訂單就會掛起,此時由sellrex函數創建一個欠條,這就是訂單失敗的由來。
3.2、 如果夠的話,則從rex_pool資金池中減去用戶收回的資金,更新相關的表字段,更新用戶的rex_balance賬戶,扣除相應的 rex 解鎖金額。

那么到這里,整個sellrex的流程都講清楚了,流程圖是這樣子的:

安全性分析

由于本次沒有拓展新的函數,所以安全結論是和上篇是一樣的,但是這次我們可以對上次說的安全問題有更深的了解。在原先版本的 rex 合約中,是沒有 check( pending_sell_order.amount <= bitr->matured_rex, "insufficient funds for current and scheduled orders" )這一個校驗的,這會導致什么呢?我們知道,當資金池中的資金不足以支付用于的賣單的時候,將跳過 if 判斷下的所有步驟,直接由 sellrex函數掛起訂單,在這種情況下,惡意用戶在系統資金池資金不足的時候,就可以一直賣rex,疊加掛起訂單的rex金額,直到資金池有足夠的資金支付,出售比購買rex數量更多的rex。但是這樣操作還是會賣不出去,因為最后更改用戶 rex_balance的時候由于asset結構體自帶的溢出檢測,是不能成功賣出去的。但是這就會讓這個訂單成為一筆壞賬,在這種情況下,因為有未完成的sellrex order,整個 REX 系統將停止運行。具體原因是什么可以自己去發現,答案會在下一篇文章揭曉。

文章可能有說得不對或說得不夠好的地方,歡迎討論交流。 詳情參考: https://eosauthority.com/blog/REX_progress_with_testing_and_implementation_details

聲明

本文僅用作技術參考,不構成任何投資建議。投資者應在充分了解相關風險的基礎上進行理性投資。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/922/