一行代碼蒸發了 ¥6,447,277,680 人民幣!

現在進入你還是先行者,最後觀望者進場才是韭菜。

美圖董事長蔡文勝曾在三點鐘群,高調的說出了這句話,隨即被大眾瘋傳。

在他發表完言論沒多久,2 月美鏈(BEC)上交易所會暴漲 4000%,後又暴跌。儘管他多次否認,聰明的網友早已扒出,他與 BEC 千絲萬縷的關係。

一行代碼蒸發了 ¥6,447,277,680 人民幣!

莊家坐莊操控幣價,美圖的股價隨之暴漲,蔡文勝順利完成了他的韭菜收割大計。

但在幣圈,割人者,人恆割之。

隨著 BEC 智能合約的漏洞的爆出,被黑客利用,瞬間套現拋售大額 BEC,60億在瞬間歸零。

而這一切,竟然是因為一個簡單至極的程序 Bug。

一行代碼蒸發了 ¥6,447,277,680 人民幣!

背景

今天有人在群裡說,Beauty Chain 美蜜代碼裡面有 bug,已經有人利用該 bug 獲得了 57,896,044,618,658,100,000,000,000,000,000,000,000,000,000,000,000,000,000,000.792003956564819968 個 BEC。

那筆操作記錄是 0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f(https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f)

一行代碼蒸發了 ¥6,447,277,680 人民幣!

我們可以看到執行的方法是 batchTransfer。

那這個方法是幹嘛的呢?(給指定的幾個地址,發送相同數量的代幣)。

整體邏輯是

  • 你傳幾個地址給我(receivers),然後再傳給我你要給每個人多少代幣(value);

  • 然後你要發送的總金額 = 發送的人數* 發送的金額;

  • 然後 要求你當前的餘額大於 發送的總金額;

  • 然後扣掉你發送的總金額;

  • 然後 給 receivers 裡面的每個人發送 指定的金額(value)。

從邏輯上看,這邊是沒有任何問題的,你想給別人發送代幣,那麼你本身的餘額一定要大於發送的總金額的!

但是這段代碼卻犯了一個很傻的錯!

代碼解釋

一行代碼蒸發了 ¥6,447,277,680 人民幣!

這個方法會傳入兩個參數:

  • _receivers

  • _value

_receivers 的值是個列表,裡面有兩個地址:

0x0e823ffe018727585eaf5bc769fa80472f76c3d7

0xb4d30cac5124b46c2df0cf3e3e1be05f42119033

_value 的值是:

8000000000000000000000000000000000000000000000000000000000000000

我們再查看代碼(如下圖):

一行代碼蒸發了 ¥6,447,277,680 人民幣!

我們一行一行地來解釋:

uint cnt = _receivers.length;

是獲取 _receivers 裡面有幾個地址,我們從上面可以看到 參數裡面只有兩個地址,所以 cnt=2,也就是 給兩個地址發送代幣。

uint256 amount = uint256(cnt) * _value;

uint256

首先 uint256(cnt) 是把 cnt 轉成了 uint256 類型。那麼,什麼是 uint256 類型?或者說 uint256 類型的取值範圍是多少?

uintx 類型的取值範圍是 0 到 2 的 x 次方 -1。也就是,假如是 uint8 的話,則 uint8 的取值範圍是 0 到 2 的 8 次方 -1,即 0 到 255。

那麼,uint256 的取值範圍是:

0 - 2 的 256 次方 -1 ,也就是 0 到 115792089237316195423570985008687907853269984665640564039457584007913129639935

Python 算 2 的 256 次方是多少?

一行代碼蒸發了 ¥6,447,277,680 人民幣!

那麼假如說 設置的值超過了 取值範圍怎麼辦?這種情況稱為“溢出”。

舉個例子來說明:

因為 uint256 的取值太大了,所以用 uint8 來 舉例。

從上面我們已經知道了 uint8 最小是 0,最大是 255。

那麼當我 255 + 1 的時候,結果是啥呢?結果會變成 0

那麼當我 255 + 2 的時候,結果是啥呢?結果會變成 1

那麼當我 0 - 1 的時候,結果是啥呢?結果會變成 255

那麼當我 0 - 2 的時候,結果是啥呢?結果會變成 254

那麼,我們回到上面的代碼中:

amount = uint256(cnt) * _value

則 amount = 2* _value。

但是此時 _value 是 16 進制的,我們把它轉成 10 進制:

(Python 16 進制轉 10 進制)

可以看到 _value = 57896044618658097711785492504343953926634992332820282019728792003956564819968

那麼 amount = _value*2 = 115792089237316195423570985008687907853269984665640564039457584007913129639936

可以在查看上面看到 uint256 取值範圍最大為 115792089237316195423570985008687907853269984665640564039457584007913129639935

此時,amout 已經超過了最大值,溢出 則 amount = 0

下一行代碼 require(cnt > 0 && cnt <= 20); require 語句是表示該語句一定要是正確的,也就是 cnt 必須大於 0 且 小於等於 20

我們的 cnt 等於 2,通過!

require(_value > 0 && balances[msg.sender] >= amount);

這句要求 value 大於 0,我們的 value 是大於 0 的 且,當前用戶擁有的代幣餘額大於等於 amount,因為 amount 等於 0,所以 就算你一個代幣沒有,也是滿足的!

balances[msg.sender] = balances[msg.sender].sub(amount);

這句是當前用戶的餘額 - amount

當前 amount 是 0,所以當前用戶代幣的餘額沒有變動。

for (uint i = 0; i < cnt; i++) {balances[_receivers[i]] = balances[_receivers[i]].add(_value);Transfer(msg.sender, _receivers[i], _value);

}

這句是遍歷 _receivers 中的地址, 對每個地址做以下操作:

balances[_receivers[i]] = balances[_receivers[i]].add(_value); _receivers 中的地址 的餘額 = 原本餘額+value

所以 _receivers 中地址的餘額 則加了 57896044618658097711785492504343953926634992332820282019728792003956564819968 個代幣!!!

Transfer(msg.sender, _receivers[i], _value); } 這句則只是把贈送代幣的記錄存下來!!!

總結

就一個簡單的溢出漏洞,導致 BEC 代幣的市值接近歸 0。

那麼,開發者有沒有考慮到溢出問題呢?

其實他考慮了,可以看如下的截圖:

一行代碼蒸發了 ¥6,447,277,680 人民幣!

除了 amount 的計算外, 其他的給用戶轉錢都用了 safeMath 的方法(sub,add)。

那麼,為啥就偏偏這一句沒有用 safeMath 的方法呢。。。

這就要問寫代碼的人了。。。

啥是 safeMath

一行代碼蒸發了 ¥6,447,277,680 人民幣!

safeMath 是為了計算安全 而寫的一個 library。

我們看看它幹了啥?為啥能保證計算安全。

function mul(uint256 a, uint256 b) internal constant returns (uint256) {uint256 c = a * b;assert(a == 0 || c / a == b);return c;

}

如上面的乘法。他在計算後,用 assert 驗證了下結果是否正確!

如果在上面計算 amount 的時候,用了 mul 的話, 則 c / a == b 也就是 驗證 amount / cnt == _value。

這句會執行報錯的,因為 0 / cnt 不等於 _value。

所以程序會報錯!

也就不會發生溢出了...

那麼,還有一個小問題,這裡的 assert 好 require 好像是乾的同一件事 —— 都是為了驗證某條語句是否正確!

那麼它倆有啥區別呢?

  • 用了 assert 的話,則程序的 gas limit 會消耗完畢;

  • 而 require 的話,則只是消耗掉當前執行的 gas。

總結

那麼 我們如何避免這種問題呢?

我個人看法是:

  • 只要涉及到計算,一定要用 safeMath

  • 代碼一定要測試!

  • 代碼一定要 review!

  • 必要時,要請專門做代碼審計的公司來測試代碼。

這件事後需要如何處理呢?

目前,該方法已經暫停了(還好可以暫停)所以看過文章的朋友 不要去測試了...

一行代碼蒸發了 ¥6,447,277,680 人民幣!

不過已經發生了的事情咋辦呢?

我的想法是,快照在漏洞之前,所有用戶的餘額情況,然後發行新的 token,給之前的用戶發送等額的代幣...


分享到:


相關文章: