知道創宇區塊鏈安全實驗室 | Grim Finance閃電貸安全事件分析

0x01:前言

援引官方訊息,北京時間12月19日,Fantom鏈上覆合收益平臺GrimFinance遭遇了閃電貸攻擊。

知道創宇區塊鏈安全實驗室

第一時間對本次事件深入跟蹤並進行分析。

0x02:事件詳情

交易細節如下圖所示:

知道創宇區塊鏈安全實驗室 | Grim Finance閃電貸安全事件分析

瀏覽上圖的交易過程可知,攻擊合約(0xb08ccb39741d746dd1818641900f182448eb5e41)利用閃電貸借取代幣,將借取的代幣質押到SpiritSwap裡增加流動性獲取lp代幣,而問題就出現在depositFor()函式中。

透過Tenderly(https://dashboard。tenderly。co/tx/fantom/0x19315e5b150d0a83e797203bb9c957ec1fa8a6f404f4f761d970cb29a74a5dd6/debugger 除錯該筆交易,攻擊者多次遞迴呼叫depositFor函式,利用該函式獲取大量代幣:

知道創宇區塊鏈安全實驗室 | Grim Finance閃電貸安全事件分析

0x03:漏洞分析

depositFor() 函式位於https://ftmscan。com/address/0x660184ce8af80e0b1e5a1172a16168b15f4136bf#code 的第1115行:

function depositFor(address token, uint _amount,address user ) public { uint256 _pool = balance(); IERC20(token)。safeTransferFrom(msg。sender, address(this), _amount); earn(); uint256 _after = balance(); _amount = _after。sub(_pool); // Additional check for deflationary tokens uint256 shares = 0; if (totalSupply() == 0) { shares = _amount; } else { shares = (_amount。mul(totalSupply()))。div(_pool); } _mint(user, shares);}

該函式的safeTransferFrom() 方法從 IERC20(token) 呼叫,最後一次呼叫,也就是逆序第一次執行後,餘額balance也會隨之變動。當前鑄造憑證數量/前一筆鑄造憑證數量固定為3。54:

知道創宇區塊鏈安全實驗室 | Grim Finance閃電貸安全事件分析

我們推導其公式為:

知道創宇區塊鏈安全實驗室 | Grim Finance閃電貸安全事件分析

將該公式分子拆分,得到shares/totalSupply的固定比為2。54:

知道創宇區塊鏈安全實驗室 | Grim Finance閃電貸安全事件分析

最後將Debug交易裡的shares/totalSupply進行計算,其值與shares/totalSupply的固定比相同,因此可以確定套利值只與totalSupply()有關:

知道創宇區塊鏈安全實驗室 | Grim Finance閃電貸安全事件分析

其中函式safeTransferFrom()傳入的變數token可控,導致攻擊者可以自己實現safeTransferFrom() 方法,將該方法重入到depositFor(),拉昇totalSupply()總量,最後透過_mint()方法向用戶新增質押憑證實現套利。

以實施了5次重入攻擊為例,開始pool的值為0,在重入depositFor方法的前四次裡,攻擊者一直傳入自己鑄造的代幣,pool的值會一直保持為0,但在第五次,也就是最後一次傳入100個受認可的代幣時,after的值會變成100,而afer-pool的差值amount也就是100,最後由於重入了5次,導致合約會向攻擊者鑄造100*5的質押憑證代幣。

知道創宇區塊鏈安全實驗室 | Grim Finance閃電貸安全事件分析

知道創宇區塊鏈安全實驗室 | Grim Finance閃電貸安全事件分析

知道創宇區塊鏈安全實驗室 | Grim Finance閃電貸安全事件分析

知道創宇區塊鏈安全實驗室 | Grim Finance閃電貸安全事件分析

其後果就是攻擊者只質押一次代幣,仍能多次增加質押總量實現套利。

0x04:修復方案

1。 由於depositFor()方法裡的token可控才是導致這次攻擊事件的原因,因此只需要在傳遞引數的時候讓token不可控就行:

function depositFor( uint _amount,address user ) public

2。 由於套利的原因是depositFor()方法裡存在修改代幣數量的函式,因此還可以將修改代幣的方法單獨實現,這樣即使token變數可控,也無法成功套利:

function depositFor(address token, uint _amount,address user ) public { IERC20(token)。safeTransferFrom(msg。sender, address(this), _amount);}

3。 鎖定交易token:

function setLPToken(address lp) public onlyOwner { lpToken = lp;}function depositFor(uint _amount,address user ) public { uint256 _pool = balance(); IERC20(lpToken)。safeTransferFrom(msg。sender, address(this), _amount); earn(); ……}

0x05:總結

經過完整分析,

知道創宇區塊鏈安全實驗室

明確了該次攻擊事件的源頭並非網傳的閃電貸攻擊,攻擊者利用GrimBoostVault合約的depositFor方法引數可控,實施了重入攻擊,將自己的鑄造的無價值代幣兌換成了質押憑證,最後透過withdrawAll方法實現套利,而閃電貸?攻擊者只是利用閃電貸擴大了套利值。

對於合約程式碼而言安全性是十分重要的,每一個未經驗證的傳入引數都可能導致巨大的經濟損失,開發者在編寫重要操作方法時,須記住零信任原則,謹慎對待每一個傳入引數。