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

前言

伴隨著 REX 提案終于被 BP 們投票通過,炒了半年概念的 REX 終于上線了,這個號稱穩賺不虧的投資項目吸引了眾多人的目光,同時也霸占了各大區塊鏈媒體的頭條,其火熱程度不亞于平臺幣,一上線便涌入了大量的資金。但是 REX 究竟是什么呢?REX 又有什么用?本系列基于 rex1.6.0-rc2 源碼進行分析,給出相關的細節及答案。

什么是 REX

REX,全稱 Resource Exchange,即資源交易所,是為了提供一個更好的資源租賃平臺,緩解EOS高昂的資源使用成本,以更少的 EOS 換取更多的資源。同時也可以增加用戶投票,促進 EOS 系統的良性運轉。現在市面上有許多資源租賃 DApp,目的也是為了用于緩解 CPU 緊缺的問題。REX 與這些平臺一樣,都是充當租賃平臺的角色,不同的是資源出租方不再是 DApp,而是每一個 EOS 持有者都能成為資源出租方,并享受收益。這里需要重點聲明的是,REX 不是一種代幣,而是一個資源租賃平臺!用戶購買的 REX 只是流轉于 REX 租賃平臺內的一種通證,用于證明用戶出租了資源,這種通證 本身不可流轉,無法交易。類似于國債,REX 就是手中的債券。為了區分這兩個概念,下文統一將 REX 資源租賃平臺稱為 REX。而用戶購買得到的通證稱為 rex。

更詳細的資料可以參看BM自己的文章: https://medium.com/@bytemaster/proposal-for-eos-resource-renting-rent-distribution-9afe8fb3883a

REX 攻略

對于一般用戶而言,買賣 rex 只需要接觸到以下幾個接口,分別是: 1、depodit:用于充值,將 EOS 變成 SEOS,也叫預備金。 2、withdraw:用與提現,將 SEOS 換回 EOS。 3、buyrex:用于從用戶的預備金中扣除相應的份額,并用于 rex 的購買。 4、sellrex:用于賣出已經結束鎖定的REX,并將本金連帶收益一起放進用戶的預備金賬戶中。 5、unstaketorex:將用于抵押中的資源用于 rex 的購買 下面,我們一起來看下這幾個函數的實現,了解資金的流向

deposit 函數

deposit 函數是用戶參與 REX 的第一個接口,顧名思義,用戶充值以備后來購買 rex。就像去游戲廳充值游戲幣一樣,先把人民幣換成游戲廳的點數沖進卡里,然后用這張卡進行后續的游戲,后續的所有花費都是基于這張卡的。REX 也是相同的道理,后續所有的買賣操作都基于這個儲備金賬戶。deposit 函數的具體實現如下:

 void system_contract::deposit( const name& owner, const asset& amount )
   {
      require_auth( owner );

      check( amount.symbol == core_symbol(), "must deposit core token" );
      check( 0 < amount.amount, "must deposit a positive amount" );
      INLINE_ACTION_SENDER(eosio::token, transfer)( token_account, { owner, active_permission },
                                                    { owner, rex_account, amount, "deposit to REX fund" } );///充值進rex賬戶
      transfer_to_fund( owner, amount );///初始化用戶余額,不存在用戶則新增用戶,存在則累加金額
      update_rex_account( owner, asset( 0, core_symbol() ), asset( 0, core_symbol() ) );
   }

我們不需要了解每一行的具體實現,但是大概的道理是需要明白的。deposit 函數做了以下事情: 1、首先在第三行校驗了用戶權限,總不能平白無故的讓別人給自己買了 rex,繞過自己的意志。 2、在第五行和第六行對購買金額和代幣的信息進行校驗,不能拿假的 EOS 來買,也不能買個負數的,保證 REX 的安全性 3、把用戶的 EOS 打進 eosio.rex 賬戶,你的錢就從你的口袋,轉到了 eosio.rex 系統賬戶上了。 4、調用 transfer_to_fund 接口,把用戶的充值金額用小本本記起來,這相當我們的儲備金錢包,在數據體現上是一個表,后續將根據這個表進行 rex 的購買。 5、調用 update_rex_account 接口,這個接口在輸入不同的參數的時候有不同的功能,這里是用于處理用戶的賣單,把用戶賣 rex 得到的收益一并整理進儲備金賬戶中。

withdraw 函數

withdraw 函數是 deposit 函數的反向接口,用于將儲備金賬戶中的余額轉移到用戶的 EOS 賬戶中,就像你在游戲廳玩夠了,卡里還有點數,或玩游戲贏到點數放進卡里,就可以用卡里的點數換回人民幣,下次再來,withdraw 函數的道理也是一樣的。withdraw 函數的具體實現如下:

void system_contract::withdraw( const name& owner, const asset& amount )
   {
      require_auth( owner );

      check( amount.symbol == core_symbol(), "must withdraw core token" ); ///EOS符號校驗
      check( 0 < amount.amount, "must withdraw a positive amount" );
      update_rex_account( owner, asset( 0, core_symbol() ), asset( 0, core_symbol() ) );
      transfer_from_fund( owner, amount );
      INLINE_ACTION_SENDER(eosio::token, transfer)( token_account, { rex_account, active_permission },
                                                    { rex_account, owner, amount, "withdraw from REX fund" } );
   }

與 deposit 函數大致一樣,withdraw 函數同樣對 EOS 代幣的信息進行了相關的校驗,與 deposit 函數不一樣的是,withdraw 函數調用 update_rex_account 接口和 transfer_from_fund 接口的順序與 deposit 函數不一樣。但目的都是為了處理用戶的 rex 賣單,將收益歸結金儲備金賬戶中。分別用于提現或購買 rex。這里詳細的細節分析將放到后續文章之中。

buyrex 函數

折騰了那么久,怎么充值看完了,怎么提現也看完了,下面就到了我們最關心的問題,就是該怎么買的問題了。買 rex 調用的接口為 buyrex 函數,函數的具體實現如下:

void system_contract::buyrex( const name& from, const asset& amount )
   {
      require_auth( from );

      check( amount.symbol == core_symbol(), "asset must be core token" );
      check( 0 < amount.amount, "must use positive amount" );
      check_voting_requirement( from );//檢查用戶是否投票
      transfer_from_fund( from, amount ); //從用戶的基金中扣除,需要先通過despoit函數進行充值之后才能進行rex的購買
      const asset rex_received    = add_to_rex_pool( amount ); //計算能獲得的rex的數量
      const asset delta_rex_stake = add_to_rex_balance( from, amount, rex_received ); ///更改用戶賬戶中的rex的數量
      runrex(2);
      update_rex_account( from, asset( 0, core_symbol() ), delta_rex_stake );
      // dummy action added so that amount of REX tokens purchased shows up in action trace 
      dispatch_inline( null_account, "buyresult"_n, { }, std::make_tuple( rex_received ) );      
   }

和前面兩個函數一樣,buyrex 函數同樣也校驗了代幣的相關信息,然后使用 transfer_from_fund 函數從用戶的儲備金中扣除響應的金額。除此之外,我們還應該關注另外三個函數,分別是 check_voting_requirement,add_to_rex_pool 和 add_to_rex_balance。這三個函數分別用于檢查用戶是否投票、計算能購買到的 rex 的數量并把相應增加的 rex 數量加到 rexpool 中、記錄用戶購買的 rex 信息并計算用戶購買的 rex 的解鎖時間。那么,我們能獲取到的 rex 的數量是怎么計算出來的呢?從源碼上我們可以看到,計算 rex 的數量調用了 add_to_rex_pool 函數。所以,下面將著重分析 add_to_rex_pool 函數。

add_to_rex_pool 函數

add_to_rex_pool 函數用于將用戶購買的 rex 放進 rex_pool 中,并根據 rex_pool 中的相關信息計算出用戶能夠購買的 rex 的數量。首先我們先看下 rex_pool 表的定義

struct [[eosio::table,eosio::contract("eosio.system")]] rex_pool {
      uint8_t    version = 0;
      asset      total_lent; /// total amount of CORE_SYMBOL in open rex_loans
      asset      total_unlent; /// total amount of CORE_SYMBOL available to be lent (connector) 
      asset      total_rent; /// fees received in exchange for lent  (connector)  
      asset      total_lendable; /// total amount of CORE_SYMBOL that have been lent (total_unlent + total_lent) 
      asset      total_rex; /// total number of REX shares allocated to contributors to total_lendable
      asset      namebid_proceeds; /// the amount of CORE_SYMBOL to be transferred from namebids to REX pool
      uint64_t   loan_num = 0; /// increments with each new loan

      uint64_t primary_key()const { return 0; }
   };

以上是 rex_pool 表的定義,其中定義了8個字段,除去 version 參數,我們分別一個一個解釋每個參數的意思

1、total_lent:用于記錄總共被借出了多少的 cpu 資源和 net 資源,這個資源是以 EOS 為單位的。
2、total_unlent:記錄 rex_pool 中未用于出借的 EOS 資源。包括用戶因為購買 rex 所產生的可用于出租的金額,租用資源的用戶的租金。 這其中有一部會因為出租資源而鎖定的金額(30天后自動解鎖),是一個 connector,用于 bancor 操作,計算一定數量的 EOS 可租借的資源。
3、total_rent:用于記錄用戶在租用資源的時候支付的租金,是一個 connector,其反應了租借資源的用戶的多少。用于bancor操作,計算一定數量的 EOS 可租借的資源。
4、total_lenable:可以說是整個 rex_pool 的所有資金,計算公式為 total_unlent + total_lent。這里的資金來源還包括 name bid 的競拍費用以及 ram fee。這個參數同時和用戶的收益息息相關。
5、total_rex:rex_pool 中 rex 的總量,其來源于用戶購買 rex。
6、namebid_proceeds:記錄競拍賬戶產生的費用。
7、loan_num:記錄出租資源的總次數。

明白了以上字段的定義,我么現在正式看看 add_to_rex_pool 函數,以下是函數的具體實現。

asset system_contract::add_to_rex_pool( const asset& payment )
   {
      /**
       * If CORE_SYMBOL is (EOS,4), maximum supply is 10^10 tokens (10 billion tokens), i.e., maximum amount
       * of indivisible units is 10^14. rex_ratio = 10^4 sets the upper bound on (REX,4) indivisible units to
       * 10^18 and that is within the maximum allowable amount field of asset type which is set to 2^62
       * (approximately 4.6 * 10^18). For a different CORE_SYMBOL, and in order for maximum (REX,4) amount not
       * to exceed that limit, maximum amount of indivisible units cannot be set to a value larger than 4 * 10^14.
       * If precision of CORE_SYMBOL is 4, that corresponds to a maximum supply of 40 billion tokens.
       */
      const int64_t rex_ratio       = 10000;
      const int64_t init_total_rent = 20'000'0000; /// base amount prevents renting profitably until at least a minimum number of core_symbol() is made available
      asset rex_received( 0, rex_symbol );
      auto itr = _rexpool.begin();
      if ( !rex_system_initialized() ) {
         /// initialize REX pool
         _rexpool.emplace( _self, [&]( auto& rp ) {
            rex_received.amount = payment.amount * rex_ratio; ///計算能獲得的rex的數量
            rp.total_lendable   = payment;///由于用戶 buy rex,使得 rex pool 中有可出租的 EOS,所以 rex_lendable 為首位用戶的購買資金
            rp.total_lent       = asset( 0, core_symbol() );///初始化rex pool,暫時還沒有人借資源
            rp.total_unlent     = rp.total_lendable - rp.total_lent; ///計算還能借的
            rp.total_rent       = asset( init_total_rent, core_symbol() );
            rp.total_rex        = rex_received;
            rp.namebid_proceeds = asset( 0, core_symbol() );
         });
      } else if ( !rex_available() ) { /// should be a rare corner case, REX pool is initialized but empty
         _rexpool.modify( itr, same_payer, [&]( auto& rp ) {
            rex_received.amount      = payment.amount * rex_ratio;
            rp.total_lendable.amount = payment.amount;
            rp.total_lent.amount     = 0;
            rp.total_unlent.amount   = rp.total_lendable.amount - rp.total_lent.amount;
            rp.total_rent.amount     = init_total_rent;
            rp.total_rex.amount      = rex_received.amount;
         });
      } else {
         /// total_lendable > 0 if total_rex > 0 except in a rare case and due to rounding errors
         check( itr->total_lendable.amount > 0, "lendable REX pool is empty" );
         const int64_t S0 = itr->total_lendable.amount;
         const int64_t S1 = S0 + payment.amount;
         const int64_t R0 = itr->total_rex.amount;
         const int64_t R1 = (uint128_t(S1) * R0) / S0;
         rex_received.amount = R1 - R0; ///計算能獲得的rex
         _rexpool.modify( itr, same_payer, [&]( auto& rp ) {
            rp.total_lendable.amount = S1;
            rp.total_rex.amount      = R1;
            rp.total_unlent.amount   = rp.total_lendable.amount - rp.total_lent.amount;
            check( rp.total_unlent.amount >= 0, "programmer error, this should never go negative" );
         });
      }

      return rex_received;

首先我們看下我們能購買到的 rex 是怎么計算的。當 rex_pool 迎來第一個購買 rex 的用戶的時候,獲得 rex 的獲取比例是1:10000,即1個 EOS 換10000個 rex,往后購買 rex 的用于按照公式((uint128_t(S1) * R0) / S0) - R0計算能獲取的 rex。看起來很復雜對不對?我們對公式進行分解下,首先進行以下轉換,公式變為(S1 / S0 * R0) - R0,再代入 S1,得到((S0 + payment) / S0 * R0) - R0,最后我們進行分解再去括號,得到 R0 + (payment / S0) * R0 - R0。最后這個公式就變成了(payment / S0) * R0。再變一下,變成 payment * (R0 / S0),即用戶用于購買 rex 的資金乘以當前 rex_pool 中的 EOS 總資產與 rex_pool 中的 rex 的總量之間的比例。這個比例在沒有第三方資金如賬戶競拍費用,ram fee 等的干擾下是固定不變的,為1:10000。但是當有第三方資金入場的時候,作為分母的 S0 就會不斷變大,那么這個比例就不斷變小,同樣的金額能買到的 rex 就會越來越少。通過上面的分析,我們知道,在有第三方資金的參與下,rex 買得越早最早,能買到的數量就越多。rex 的價格與購買的人數無關,而與租借資源的數量,系統競拍資源產生的收益,以及 ram fee有關

sellrex 函數

那么,現在流程走到這里,剩下的就是計算收益的問題了。用于處理用戶出租 EOS 資源產生收益的計算細節的實現全部在 sellrex 函數中。以下是 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 函數有很多學問,完整說下來可能不是這篇短短的分析能寫完的,但是可以分析我們最關心的問題,就是獲得的收益是怎么計算出來的。首先我們不管其他細節,先看看在真正計算收益之前做了什么。主要分為以下幾步:

1、檢查用戶購買了 rex 沒有,總不能沒買就能賣對吧。
2、通過 process_rex_maturities 函數計算結束鎖定的 rex,用戶從購買的 rex 到賣 rex 需要4天的釋放期。
3、檢測需要賣出的 rex 的數量是否小于結束鎖定的REX的數量。

通過以上幾步檢查之后,就真正進入了結算函數。rex 的收益結算是通過 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 };
   }

同樣的,類似 add_to_rex_pool,我們也可以拋開其他細節,直擊最核心的收益計算公式,即第6行的計算公式。(uint128_t(rex.amount) * S0)/ R0,這個函數雖然看起來同樣的復雜,但是我們可以用相同的方法進行簡化。首先我們對公式進行一些轉換,變成 rex.amount / R0 * S0,加個括號,變成 rex.amount * (R0 / S0),即你能收益的 rex 是你要賣的 rex 乘以 rex_pool 中 rex 總量和 rex_pool 中得總 EOS 總資產之間的比例,這個比例在沒有第三方資金如 name bid 和 ram fee 加入的情況下也是維持穩定不變的10000:1。

我們知道了什么?

一口氣說了一大堆,看到這里的你可能還有點茫然,可能只是記住了兩個公式的轉化,不打緊。我來總結下這次看完文章的的收獲。通過以上的分析,我們知道買 rex 和賣 rex 都是根據 rex 總量和 rex_pool 中的 EOS 的總資金之間的比例進行計算的,也就是說在沒有第三方資金參與,用戶的 EOS 總是按1:10000的比例變成 rex,再按10000:1的比例再變成 EOS。這說明,在沒有第三方資源的情況下,rex 和 EOS 總是按照一定的比例進行互換,這也是為什么 REX 號稱穩賺不虧的原因。同時,在有第三方資金入場的時候,R0 / S0 的比例就會變小,也意味著 S0 / R0 的比例變大,雖然同樣資金買到的 rex 變少了,但是,賣出去的比例就變大了,獲得的收益就變得更多了。

整個參與的流程大致如下:

REX 安全性分析

REX 作為 EOS 本身的系統合約,其安全防護必須要做到面面俱到,一旦出現問題,將造成災難性的影響。REX 合約已經由 EOS Authority 團隊進行義務安全審計,但作為一名安全人員,筆者同時也對 REX 的整個架構進行了深入的思考,文章將會陸續對每個文章提及到的接口進行分析,闡述其安全性或安全性增強建議。

本文粗略介紹了四個接口,分別是deposit,withdraw,buyrex,sellrex。

從函數實現上來看

1、每個函數都有對 asset 參數的信息進行校驗,包括數量,代幣的符號信息是否與系統代幣信息一致。防止可能的假充值問題和溢出問題。
2、用戶的關鍵操作都有權限校驗,防止越權操作。

同時,文章內介紹的四個接口不存在 EOS 上常見的攻擊手法如回滾攻擊,排擠攻擊,假通知攻擊。

但值得注意的是,在這幾個函數中,sellrex 函數曾存在一個嚴重漏洞(現已修復),導致用于可以從 REX 中盜取資產。

詳細信息如下: https://eosauthority.com/blog/REX_progress_with_testing_and_implementation_details

漏洞的成因在于進行 sellrex 操作的時候 REX 系統可能會不夠錢支付用戶的收益,在這種情況下,用戶的賣單就會掛起,如果沒有校驗訂單,惡意用戶就能在系統資金不足的情況下一直進行 sellrex 操作,一直增加掛起訂單的金額,直到有系統有足夠的資源支付用戶的收益

結語

REX 是一個龐大的系統,不存在三言兩語將全部細節分析到位情況,文章沒有分析太多的技術細節,只是大概分析了每個函數的大概作用,介紹了關于 REX 收益最核心的地方。想要了解具體細節的朋友可以持續關注我們的系列文章~下一篇文章將會繼續說明這些函數之間更加好玩的細節!文章可能有說得不對的地方,歡迎大家指點交流。

聲明

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


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