mt logoMyToken
总市值:
0%
恐慌指数:
0%
币种:--
交易所 --
ETH Gas:--
EN
USD
APP
Ap Store QR Code

Scan Download

一文深度分析 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);
免责声明:本文版权归原作者所有,不代表MyToken(www.mytokencap.com)观点和立场;如有关于内容、版权等问题,请与我们联系。