YFValue,一行程式碼如何鎖定上億資產

買賣虛擬貨幣

By : yudan@慢霧安全團隊

前言

據鏈聞訊息,DeFi 專案 YFValue (YFV)釋出公告稱,團隊於昨日在 YFV 質押池中發現一個漏洞,惡意參與者藉此漏洞對質押中的 YFV 計時器單獨重置。目前已有一個惡意參與者正試圖藉此勒索團隊。慢霧安全團隊對此進行了深入分析,以下是相關技術細節。

細節分析

以上是 YFValue 的官方說明(來源:https://medium.com/@yfv.finance/yfv-update-staking-pool-exploit-713cb353ff7d),從宣告中我們可以得知是 YFV 抵押池出現了問題,惡意的使用者可重置 YFV 抵押者的計時器,對 YFV 的抵押者造成不便,但這並不會導致資金損失。

透過登陸 YFValue 的官方網站 ,(https://yfv.finance/staking),可以發現在 YFValue 的體系中,使用者可透過質押相關的代幣獲取對應的獎勵,目前 YFValue 支援的質押代幣池有以下幾個:

可以看到,目前由於漏洞的原因,YFV 的抵押池已經在 UI 介面關閉了抵押功能,但是合約上目前還沒關閉代幣抵押的功能,我們需要跟蹤程式碼來分析具體的細節點。

根據官網提供的 Github 地址,我們溯源到了相關的程式碼倉庫(https://github.com/yfv-finance/audit),關於 YFV 抵押的相關邏輯在YFV_Stake.sol 合約中,合約中關於抵押的函式有 2 個,分別是 stake 函式和 stakeOnBehalf 函式,以下是具體的程式碼:

function stake(uint256 amount, address referrer) public updateReward(msg.sender) checkNextEpoch {    require(amount > 0, "Cannot stake 0");    require(referrer != msg.sender, "You cannot refer yourself.");    super.tokenStake(amount);    lastStakeTimes[msg.sender] = block.timestamp;    emit Staked(msg.sender, amount);    if (rewardReferral != address(0) && referrer != address(0)) {      IYFVReferral(rewardReferral).setReferrer(msg.sender, referrer);     }   }  function stakeOnBehalf(address stakeFor, uint256 amount) public updateReward(stakeFor) checkNextEpoch {    require(amount > 0, "Cannot stake 0");    super.tokenStakeOnBehalf(stakeFor, amount);    lastStakeTimes[stakeFor] = block.timestamp;    emit Staked(stakeFor, amount);   }

透過程式碼不難發現,無論是 stake 函式還是 stakeOnBehalf 函式,邏輯基本是一樣的,首先是校驗了抵押金額不能為0,接著分別呼叫上層的 tokenStake 和 tokenStakeOnBehalf 函式。緊接著更新使用者的抵押時間。只不過 stakeOnBehalf 函式可以用於為他人抵押。tokenStake 和 tokenStakeOnBehalf 的程式碼如下:

function tokenStake(uint256 amount) internal {        _totalSupply = _totalSupply.add(amount);        _balances[msg.sender] = _balances[msg.sender].add(amount);        yfv.safeTransferFrom(msg.sender, address(this), amount);    }    function tokenStakeOnBehalf(address stakeFor, uint256 amount) internal {        _totalSupply = _totalSupply.add(amount);        _balances[stakeFor] = _balances[stakeFor].add(amount);        yfv.safeTransferFrom(msg.sender, address(this), amount);    }

可以看到這裡只是簡單的把對應的 token 用 transferFrom 的方式轉入到合約中,沒有什麼特別的邏輯點。到這裡整個抵押流程就很清晰了,接下來是收益的過程。計算使用者收益的是 stakeReward 函式,領取收益的為 withdraw 函式,程式碼分別如下:

function stakeReward() public updateReward(msg.sender) checkNextEpoch {        uint256 reward = getReward();        require(reward > 0, "Earned too little");        super.tokenStake(reward);        lastStakeTimes[msg.sender] = block.timestamp;        emit Staked(msg.sender, reward);    }        function withdraw(uint256 amount) public updateReward(msg.sender) checkNextEpoch {        require(amount > 0, "Cannot withdraw 0");        require(block.timestamp >= unfrozenStakeTime(msg.sender), "Coin is still frozen");        super.tokenWithdraw(amount);        emit Withdrawn(msg.sender, amount);}

透過分析計算收益和領取收益的程式碼,發現邏輯也很簡單,stake 函式首先是透過 updateReward 修飾器更新了使用者的獎勵,然後使用 getReward 函式計算了使用者的獎勵,並把抵押時間設定成當前區塊時間。最後,使用者在提取獎勵的時候,withdraw 函式會首先計算當前的區塊時間,再與 unfrozenStakeTime 函式中計算出的時間進行對比,只有當前區塊時間大於 unfrozenStakeTime 計算出的時間,才允許提現。unfrozenStakeTime 的程式碼如下:

 function unfrozenStakeTime(address account) public view returns (uint256) {        return lastStakeTimes[account] + FROZEN_STAKING_TIME;    }

從程式碼中得知,unfrozenStakeTime 是使用使用者的上次抵押時間加上 FROZEN_STAKING_TIME 常量得出鎖定時間,只要超過時間,就能透過 withdraw 函式提現收益。整個抵押和領取收益的簡化流程如下:

分析了一大堆,回到我們最初的問題,惡意的使用者是怎麼鎖定其他使用者的資產的呢?

回到使用者抵押的邏輯,可以發現抵押邏輯中的 stakeOnBehalf 函式本意是幫助進行抵押,但是這裡有個問題,如果這個使用者先前已經有抵押了呢?那透過對已經抵押的使用者再次進行抵押,比方說抵押 1 個 YFV,是不是就能以極低的成本重置已抵押的使用者的計時器,導致使用者在 withdraw 時無法成功呼叫。更進一步,假設 YFV 抵押使用者已經成功呼叫了 stakeReward 函式,在快要達到 unfrozenStakeTime 所規定的時間時,惡意的使用者可以透過 stakeOnBehalf 函式給這個使用者抵押少量資產,即可再次對抵押獎勵進行鎖定,理論上這樣往復迴圈,即可使使用者無法取出自己的資產,但這個問題並不會導致資金損失。攻擊流程如下:

前車之鑑

這是本月出現的第二個沒有經過審計的 DeFi 專案所暴露出的風險,根據 YFValue 的官方宣告(https://medium.com/@yfv.finance/yfv-bringing-true-value-to-yield-farming-bddc4edf889a),專案程式碼是由富有經驗的開發者進行開發的,同時借鑑了其他成功的專案的程式碼,但是仍無可避免的出現了風險。術業有專攻,安全審計一方面需要專案方的正向思維,另一方面,還是需要專業的安全團隊的逆向思維,從專業的駭客角度進行模擬對抗,發現問題。

修復方案

透過分析程式碼和漏洞細節,針對本次漏洞,修復方案也很簡單,只要在抵押的時候檢查使用者的抵押狀態是否為已經抵押,如果已經抵押,則不允許再次抵押。或者對每次的抵押進行單獨的處理,不能對先前的抵押狀態產生影響。

免責聲明:

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

推荐阅读

;