一文深度分析 Ref Finance 安全事件

買賣虛擬貨幣

1. Ref Finance攻擊事件摘要

2021年8月14日,UTC時間下午2點左右,Ref Finance的核心團隊注意到了REF-NEAR交易對中存在的不尋常行為。經過調查發現,在其最近部署的收益耕作專案(Ref Farming)中發現了一個漏洞。不幸的是,已有幾位Ref Finance的使用者利用了該漏洞,並導致了該交易池中近1,000,000個REF與580,000個NEAR受到了影響。

2. 背景知識

2.1 流動性交易池(The Liquidity Pools)

在DeFi中,流動性交易池是鎖定在智慧合約中的代幣池,它促進了有效的資產交易,同時允許投資者賺取一定的收益回報。一個典型的流動性交易池通常包含兩種或以上代幣,例如DAI/ETH就是Uniswap上一個較為流行的流動性池。在該流動性池中,這兩個幣種的總價值為1:1。比如總價值1000美元的DAI/ETH交易對的資金池中有1個ETH和500個DAI。此時我們可以得出一個ETH的價值是500美元,一個DAI的價值為1美元。任何時候流動性提供者(Liquidity Provider,簡稱LP)都可以向資金池中以1:1的價值放入兩種代幣,並獲得合約所提供的LP代幣作為流動性提供者擁有部分該資金池的證明。

當該資金池中產生了交易時,合約會收取一定的手續費(例如Uniswap中的0.3%),並最終作為回報分發給該資金池的流動性提供者。

如今流動性交易池已是去中心化金融中自動做市商(AMM)、借貸協議、收益耕種、鏈上保險、加密合成資產以及區塊鏈遊戲等專案的重要組成部分。

流動性交易池的價格機制

不同的交易池有不同的價格機制,如Uniswap使用如下機制:假設流動性交易池中有X個DAI和Y個ETH。合約保證了X * Y在交易前後不變(恆定乘積)。也就是當我們需要用Δy個ETH購買DAI的時候,合約演算法的設定會給我們Δx個DAI,並使得如下等式成立:

X * Y = (X - Δx) * (Y + Δy)

2.2 Ref.Finance 專案

Ref Finance是一個基於NEAR協議的多用途去中心化金融(DeFi)平臺。其中Ref_Exchange 是其主要合約,實現了流動性交易池。而該池子的代幣即為REF, 也在本次攻擊事件中受到影響。

Ref_Exchange專案參考了Uniswap 的相關設計並用Rust語言基於NEAR協議實現。如下是該合約 ref-exchange/src/simple_pool.rs 中SimplePool 的資料結構詳細定義:

pub struct SimplePool {    /// List of tokens in the pool.    pub token_account_ids: Vec<AccountId>,    /// How much NEAR this contract has.    pub amounts: Vec<Balance>,    /// Volumes accumulated by this pool.    pub volumes: Vec<SwapVolume>,    /// Fee charged for swap (gets divided by FEE_DIVISOR).    pub total_fee: u32,    /// Portion of the fee going to exchange.    pub exchange_fee: u32,    /// Portion of the fee going to referral.    pub referral_fee: u32,    /// Shares of the pool by liquidity providers.    pub shares: LookupMap<AccountId, Balance>,    /// Total number of shares.    pub shares_total_supply: Balance,}

其中SimplePool.shares中所儲存的資料可以跟蹤該交易池中流動性提供者(LP)所佔有的的份額。

合約可以透過呼叫如下兩種方式來更改LP所提供的流動性份額:

add_liquidity

remove_liquidity (在本次攻擊事件中被利用)

3 攻擊事件資金流向分析

3.1 異常交易資訊跟蹤:

本次攻擊事件,Ref Finance在其官方Twitter賬號中羅列了部分存在可疑交易行為的使用者賬戶。我們針對這些賬戶做了進一步分析:

2cb92762bb52ba8310a5571c3d766583f0db8534f500d44864ce2c3f45dccfb5該賬戶所有的交易都是從vuhatyphu.near接收NEAR,然後轉發給另一個賬戶:7747991786f445efb658b69857eadc7a57b6b475beec26ed14da8bc35bb2b5b6

082bfe48b461dca9b9a290894cd01d24364db393e1cb25625f5f2a94df869cb8該賬戶所有的交易都是從phamteo2.near接收NEAR,然後轉發給另一個賬戶:7747991786f445efb658b69857eadc7a57b6b475beec26ed14da8bc35bb2b5b6這些代幣最初都來自於phamthien.near轉賬給phamteo2.near,總計19499.8NEAR

022558709020ee7243f010ac42b181af2481699a0e3878c0a603594707dffa8c該賬戶所有的交易都是從phamthien.near接收NEAR,然後轉發給另一個賬戶:7747991786f445efb658b69857eadc7a57b6b475beec26ed14da8bc35bb2b5b6

b8cd68c1b4989ccde3ef7d6669fdb886d6c18a36293b9a5af2a468add96e6204該賬戶所有的交易(僅一筆)都是從okok1234.near接收NEAR,然後轉發給另一個賬戶:601483a1b22699b636f1df800b9b709466eba4e1d5ce7c2e1e20317af8bbd1f3

fd04d5f11af3fb8c48ff053a6970ca9cce39d3a2e2b8b1a52cc129e93b8c59e1該賬戶所有的交易都是從phamteo3.near,或phamteo4.near接收NEAR,然後轉發給另一個賬戶:601483a1b22699b636f1df800b9b709466eba4e1d5ce7c2e1e20317af8bbd1f3其中phamteo3.near或phamteo4.near最初所獲得的NEAR代幣均來自於phamthien.near

c083bd024f2a7f44325e647fd2ff3eb8bdf1a8f22b64a1b30e58dbf3c6372ac3該賬戶所有的交易都是從phamthien.near接收NEAR,然後轉發給另一個賬戶:7747991786f445efb658b69857eadc7a57b6b475beec26ed14da8bc35bb2b5b6

0a420c5ccdd8681b4c934e0c87d04bad57e73af961498b1ebbc09594d861e0d5該賬戶所有的交易都是從phamthien.near接收NEAR,然後轉發給另一個賬戶:7747991786f445efb658b69857eadc7a57b6b475beec26ed14da8bc35bb2b5b6

4e258682b967831a34a01395d3dccde7b2f7d0435c1010062163e4686ae25cf5該賬戶所有的交易都是從tuannguyen261090.near接收NEAR,然後轉發給另一個賬戶:7747991786f445efb658b69857eadc7a57b6b475beec26ed14da8bc35bb2b5b6

以上交易中的NEAR賬戶最初所獲得的NEAR代幣均來自於如下使用者賬號,即本次攻擊事件中的最初獲利者:

vuhatyphu.near

tuannguyen261090.near

icanfixit.near

willpha.near

okok1234.near

phamthien.near

3.2 異常交易內容分析

phamthien.near作為本次攻擊事件中的最初獲利者之一,該使用者最早的一筆攻擊合約交易位於交易雜湊:7AY55CZBU1jB9b8Nn6ftnVofiD6DiMgaxhWtUaM1Gxnt,該筆交易呼叫了swap()函式,實現了ref-exchange合約中的代幣轉換。

隨後該使用者多次呼叫remove_liquidity()合約函式,該函式的作用是減少流動性提供者(LP)在流動性交易池中所提供的流動性份額(Liquidity Share),贖回之前注入交易池中的資金。

例如最早的remove_liquidity交易位於交易雜湊:6GdM4ApiVrLFkJZa3mdAG6rgxBzuCtRASiaygDprWebR

4. 攻擊原因分析

此攻擊事件源於Ref_Exchange專案合約中引入的一個不正確的熱修復補丁(HotFix)。如下是針對該補丁內容的詳細分析,以及攻擊者利用該漏洞獲利的方式說明。

4.1 關鍵合約函式的利用

上文中提到phamthien.near多次呼叫了合約函式remove_liquidity,該函式的作用為:在該資金池中減少某一流動性提供者LP所指定的流動性份額。其實現位於:ref-exchange/src/simple_pool.rs。細節如下:

/// 2020-09-20    /// Removes given number of shares from the pool and returns amounts to the parent.    pub fn remove_liquidity(        &mut self,        sender_id: &AccountId,        shares: Balance,        min_amounts: Vec<Balance>,    ) -> Vec<Balance> {        let prev_shares_amount = self.shares.get(&sender_id).expect("ERR_NO_SHARES");        assert!(prev_shares_amount >= shares, "ERR_NOT_ENOUGH_SHARES");        let mut result = vec![];        for i in 0..self.token_account_ids.len() {            let amount = (U256::from(self.amounts[i]) * U256::from(shares)                / U256::from(self.shares_total_supply))            .as_u128();            assert!(amount >= min_amounts[i], "ERR_MIN_AMOUNT");            self.amounts[i] -= amount;            result.push(amount);        }        if prev_shares_amount == shares {            // [AUDIT_13] never unregister a LP when he remove liqudity.            self.shares.insert(&sender_id, &0);        } else {            self.shares                .insert(&sender_id, &(prev_shares_amount - shares));        }        env::log(            format!(                "{} shares of liquidity removed: receive back {:?}",                shares,                result                    .iter()                    .zip(self.token_account_ids.iter())                    .map(|(amount, token_id)| format!("{} {}", amount, token_id))                    .collect::<Vec<String>>(),            )            .as_bytes(),        );        self.shares_total_supply -= shares;        result    }

函式首先從交易池的合約資料SimplePool.share中查詢函式引數傳入 send_id 所指定使用者的流動性佔比份額,並存入prev_shares_amount (line 9)

如果使用者取出全部的流動性份額,原則上應該將SimplePool.share該賬戶的份額清零 (line 22)

如果使用者取出部分的流動性份額,則將儲存取出使用者所指定份額後的剩餘份額,即prev_shares_amount - shares (line 25)

從該交易池所提供的所有流動性統計中減去該使用者取出的份額,即SimplePool.shares_total_supply -= shares (line 39)

4.1.2 攻擊事件的直接原因分析

而在此次攻擊事件發生前,存在一個不正確的HotFix:

ref-exchange/src/simple_pool.rs  @@ -182,7 +182,8 @@ impl SimplePool {             result.push(amount);         }         if prev_shares_amount == shares {-            self.shares.remove(&sender_id);+            // HotFix_lp_unregister+            // self.shares.remove(&sender_id);         } else {             self.shares                 .insert(&sender_id, &(prev_shares_amount - shares));

該HotFix提交https://github.com/ref-finance/ref-contracts/issues/37,簡稱PR#37, 可以看到該HotFix在合約中臨時取消了當LP使用者想要從該流動性池中完全去除所有的流行性時,所需執行的SimplePool.shares.remove(&sender_id)的操作。

為此使用者可以利用該合約的漏洞,在該合約資料SimplePool.share中所儲存的該賬戶流動性份額始終無法清零的情況下,反覆套利。

4.1.3 攻擊事件的根本原因分析

之所以存在該HotFix的原因則來自於該專案的另一個更早的Pull Request(https://github.com/ref-finance/ref-contracts/issues/36)。

該Pull Request指出了Ref-exchange中存在的一個缺陷,該缺陷的具體內容描述為:

如下模擬使用者Alice在Ref.Finance平臺中的正常交易使用場景:

假設使用者Alice首先往某個確定的流動性交易池中(例如編號pool#1429)新增了一定的流動性,並獲得了1.0個LP代幣;

隨後Alice質押了其中的0.5個LP代幣用於收益耕種。

在收益耕種的過程中,Alice還將剩下的0.5個LP代幣從交易池取出,即remove_liquidity,此時Alice在該交易池中的流動性份額應當為0。

但是在PR#37提交之前,當此時Alice請求去除pool#1429流動性交易池中所有的流動性時,合約的具體實現將呼叫SimplePool.shares.remove(&sender_id)從流動性交易池所維護的LP名單中直接刪除Alice使用者。

因此,後續當Alice想要取回之前質押在收益耕種專案中LP代幣時,由於該LP代幣發行的流動性交易池中pool#1429已經沒有Alice這一LP賬戶。因此會產生ERR13_LP_NOT_REGISTERED錯誤;

為了解決該缺陷,marco-sundsk暫時取消了呼叫SimplePool.shares.remove(&sender_id)刪除LP使用者這一行為,並在Ref-exchage的0.2.2版本中提交了該HotFix,並由此最終導致上述合約漏洞被利用事件的發生。

而正確做法應當把該LP使用者的流動性份額歸零,並保留該賬戶。具體的內容如下:

@@ -182,7 +183,8 @@ impl SimplePool {             result.push(amount);         }         if prev_shares_amount == shares {-             self.shares.remove(&sender_id);+            // [AUDIT_13] never unregister a LP when he remove liqudity.+            self.shares.insert(&sender_id, &0);

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;