慢霧:DeFi 當紅項目 YAM 閃電折戟,一行代碼如何蒸發數億美元? - 冷萃財經

慢霧:DeFi 當紅項目 YAM 閃電折戟,一行代碼如何蒸發數億美元?

文章摘要:當紅流動性挖礦項目 YAM 創始人披露該項目合約漏洞,慢霧技術詳解漏洞細節。

當紅流動性挖礦項目 YAM 創始人披露該項目合約漏洞,慢霧技術詳解漏洞細節。

原文標題:《DeFi YAM,一行代碼如何蒸發數億美元?》
 撰文:yudan @ 慢霧安全團隊

慢霧:DeFi 當紅項目 YAM 閃電折戟,一行代碼如何蒸發數億美元?

發生了什麼?

慢霧:DeFi 當紅項目 YAM 閃電折戟,一行代碼如何蒸發數億美元?

以上是 YAM 官方對本次事件的 簡短說明

簡單來說就是官方在合約中發現負責調整供應量的函數發生了問題,這個問題導致多餘的 YAM 代幣放進了 YAM 的 reserves 合約中,並且如果不修正這個問題,將會導致 YAM 的後續治理變為不可能。同時,官方給出了此次漏洞的具體問題代碼,如下:

慢霧:DeFi 當紅項目 YAM 閃電折戟,一行代碼如何蒸發數億美元?

從上圖可知,由於編碼不規範,YAM 合約在調整 totalSupply 的時候,本應將最後的結果除以 BASE 變數,但是在實際開發過程中卻忽略了,導致 totoalSupply 計算不正確,比原來的值要大 10^18 倍。但是代幣供應量問題和治理是怎麼扯上關係呢?這需要我們針對代碼做進一步的分析。

YAM 會變成怎樣?

為了深入了解此次漏洞造成的影響,需要對 YAM 項目代碼進行深入的了解。根據官方給出的問題代碼及項目 Github 地址(https://github.com/yam-finance/yam-protocol),可以定位出調整供應量的 rebase 函數位於 YAMDelegator.sol 合約中,具體代碼如下:

function rebase( uint256 epoch, uint256 indexDelta, bool positive ) external returns (uint256) { epoch; indexDelta; positive; delegateAndReturn(); } 

通過跟蹤 rebase 函數,發現 rebase 函數最終調用了 delegateAndReturn 函數,代碼如下:

 function delegateAndReturn() private returns (bytes memory) { (bool success, ) = implementation.delegatecall(msg.data); assembly { let free_mem_ptr := mload(0x40) returndatacopy(free_mem_ptr, 0, returndatasize) switch success case 0 { revert(free_mem_ptr, returndatasize) } default { return(free_mem_ptr, returndatasize) } } } 

通過分析代碼,可以發現 delegateAndReturn 函數最終使用 delegatecall 的方式調用了 implementation 地址中的邏輯,也就是說,這是一個可升級的合約模型。而真正的 rebase 邏輯位於 YAM.sol 中 , 繼續跟進 rebase 函數的具體邏輯,如下:

function rebase( uint256 epoch, uint256 indexDelta, bool positive ) external onlyRebaser returns (uint256) { if (indexDelta == 0) { emit Rebase(epoch, yamsScalingFactor, yamsScalingFactor); return totalSupply; } uint256 prevYamsScalingFactor = yamsScalingFactor; if (!positive) { yamsScalingFactor = yamsScalingFactor.mul(BASE.sub(indexDelta)).div(BASE); } else { uint256 newScalingFactor = yamsScalingFactor.mul(BASE.add(indexDelta)).div(BASE); if (newScalingFactor <_maxScalingFactor()) { yamsScalingFactor = newScalingFactor; } else { yamsScalingFactor =_maxScalingFactor(); } } //SlowMist// 問題代碼 totalSupply = initSupply.mul(yamsScalingFactor); emit Rebase(epoch, prevYamsScalingFactor, yamsScalingFactor); return totalSupply; } } 

通過分析最終的 rebase 函數的邏輯,不難發現代碼中根據 yamsScalingFactor 來對 totalSupply 進行調整,由於 yamsScalingFactor 是一個高精度的值,在調整完成後應當除以 BASE 來去除計算過程中的精度,獲得正確的值。但是項目方在對 totalSupply 進行調整時,竟忘記了對計算結果進行調整,導致了 totalSupply 意外變大,計算出錯誤的結果。

分析到這裡還沒結束,要將漏洞和社區治理關聯起來,需要對代碼進行進一步的分析。通過觀察 rebase 函數的修飾器,不難發現此處限定了只能是 rebaser 進行調用。而 rebaser 是 YAM 中用與實現供應量相關邏輯的合約,也就是說,是 rebaser 合約最終調用了 YAM.sol 合約中的 rebase 函數。通過跟蹤相關代碼,發現 rebaser 合約中對應供應量調整的邏輯為 rebase 函數,代碼如下:

function rebase() public { // EOA only require(msg.sender == tx.origin); // ensure rebasing at correct time _inRebaseWindow(); // This comparison also ensures there is no reentrancy. require(lastRebaseTimestampSec.add(minRebaseTimeIntervalSec) < now); // Snap the rebase time to the start of this window. lastRebaseTimestampSec = now.sub( now.mod(minRebaseTimeIntervalSec)).add(rebaseWindowOffsetSec); epoch = epoch.add(1); // get twap from uniswap v2; uint256 exchangeRate = getTWAP(); // calculates % change to supply (uint256 offPegPerc, bool positive) = computeOffPegPerc(exchangeRate); uint256 indexDelta = offPegPerc; // Apply the Dampening factor. indexDelta = indexDelta.div(rebaseLag); YAMTokenInterface yam = YAMTokenInterface(yamAddress); if (positive) { require(yam.yamsScalingFactor().mul(uint256(10**18).add(indexDelta)).div(10**18) < yam.maxScalingFactor(), "new scaling factor will be too big"); } //SlowMist// 取當前 YAM 代幣的供應量 uint256 currSupply = yam.totalSupply(); uint256 mintAmount; // reduce indexDelta to account for minting //SlowMist// 計算要調整的供應量 if (positive) { uint256 mintPerc = indexDelta.mul(rebaseMintPerc).div(10**18); indexDelta = indexDelta.sub(mintPerc); mintAmount = currSupply.mul(mintPerc).div(10**18); } // rebase //SlowMist// 調用 YAM 的 rebase 邏輯 uint256 supplyAfterRebase = yam.rebase(epoch, indexDelta, positive); assert(yam.yamsScalingFactor() <= yam.maxScalingFactor()); // perform actions after rebase //SlowMist// 進入調整邏輯 afterRebase(mintAmount, offPegPerc); } 

通過分析代碼,可以發現函數在進行了一系列的檢查後,首先獲取了當前 YAM 的供應量,計算此次的鑄幣數量,然後再調用 YAM.sol 中的 rebase 函數對 totalSupply 進行調整,也就是說 rebase 過後的對 totalSupply 的影響要在下一次調用 rebaser 合約的 rebase 函數才會生效。最後 rebase 函數調用了 afterRebase 函數。我們繼續跟進 afterRebase 函數中的代碼:

function afterRebase( uint256 mintAmount, uint256 offPegPerc ) internal { // update uniswap UniswapPair(uniswap_pair).sync(); //SlowMist// 通過 uniswap 購買 yCRV 代幣  if (mintAmount > 0) { buyReserveAndTransfer( mintAmount, offPegPerc ); } // call any extra functions //SlowMist// 社區管理調用 for (uint i = 0; i < transactions.length; i++) { Transaction storage t = transactions[i]; if (t.enabled) { bool result = externalCall(t.destination, t.data); if (!result) { emit TransactionFailed(t.destination, i, t.data); revert("Transaction Failed"); } } } } 

通過分析發現, afterRebase 函數主要的邏輯在 buyReserveAndTransfer 函數中,此函數用於將增發出來的代幣的一部分用於到 Uniswap 中購買 yCRV 代幣。跟蹤 buyReserveAndTransfer 函數,代碼如下:

 * 

function buyReserveAndTransfer( uint256 mintAmount, uint256 offPegPerc ) internal { UniswapPair pair = UniswapPair(uniswap_pair); YAMTokenInterface yam = YAMTokenInterface(yamAddress); // get reserves (uint256 token0Reserves, uint256 token1Reserves, ) = pair.getReserves(); // check if protocol has excess yam in the reserve uint256 excess = yam.balanceOf(reservesContract); //SlowMist// 計算用於 Uniswap 中兌換的 YAM 數量 uint256 tokens_to_max_slippage = uniswapMaxSlippage(token0Reserves, token1Reserves, offPegPerc); UniVars memory uniVars = UniVars({ yamsToUnitokens_to_max_slippage, // how many yams uniswap needs amountFromReservesexcess, // how much of yamsToUni comes from reserves mintToReserves0 // how much yams protocol mints to reserves }); // tries to sell all mint + excess // falls back to selling some of mint and all of excess // if all else fails, sells portion of excess // upon pair.swap, `uniswapV2Call` is called by the uniswap pair contract if (isToken0) { if (tokens_to_max_slippage > mintAmount.add(excess)) { // we already have performed a safemath check on mintAmount+excess // so we dont need to continue using it in this code path // can handle selling all of reserves and mint uint256 buyTokens = getAmountOut(mintAmount + excess, token0Reserves, token1Reserves); uniVars.yamsToUni = mintAmount + excess; uniVars.amountFromReserves = excess; // call swap using entire mint amount and excess; mint 0 to reserves pair.swap(0, buyTokens, address(this), abi.encode(uniVars)); } else { if (tokens_to_max_slippage > excess) { // uniswap can handle entire reserves uint256 buyTokens = getAmountOut(tokens_to_max_slippage, token0Reserves, token1Reserves); // swap up to slippage limit, taking entire yam reserves, and minting part of total //SlowMist// 將多餘代幣鑄給 reserves 合約 uniVars.mintToReserves = mintAmount.sub((tokens_to_max_slippage - excess)); //SlowMist// Uniswap 代幣交換 pair.swap(0, buyTokens, address(this), abi.encode(uniVars)); } else { // uniswap cant handle all of excess uint256 buyTokens = getAmountOut(tokens_to_max_slippage, token0Reserves, token1Reserves); uniVars.amountFromReserves = tokens_to_max_slippage; uniVars.mintToReserves = mintAmount; // swap up to slippage limit, taking excess - remainingExcess from reserves, and minting full amount // to reserves pair.swap(0, buyTokens, address(this), abi.encode(uniVars)); } } } else { if (tokens_to_max_slippage > mintAmount.add(excess)) { // can handle all of reserves and mint uint256 buyTokens = getAmountOut(mintAmount + excess, token1Reserves, token0Reserves); uniVars.yamsToUni = mintAmount + excess; uniVars.amountFromReserves = excess; // call swap using entire mint amount and excess; mint 0 to reserves pair.swap(buyTokens, 0, address(this), abi.encode(uniVars)); } else { if (tokens_to_max_slippage > excess) { // uniswap can handle entire reserves uint256 buyTokens = getAmountOut(tokens_to_max_slippage, token1Reserves, token0Reserves); // swap up to slippage limit, taking entire yam reserves, and minting part of total //SlowMist// 增發的多餘的代幣給 reserves 合約 uniVars.mintToReserves = mintAmount.sub( (tokens_to_max_slippage - excess)); // swap up to slippage limit, taking entire yam reserves, and minting part of total //Slowist// 在 uniswap 中進行兌換,並最終調用 rebase 合約的 uniswapV2Call 函數 pair.swap(buyTokens, 0, address(this), abi.encode(uniVars)); } else { // uniswap cant handle all of excess uint256 buyTokens = getAmountOut(tokens_to_max_slippage, token1Reserves, token0Reserves); uniVars.amountFromReserves = tokens_to_max_slippage; uniVars.mintToReserves = mintAmount; // swap up to slippage limit, taking excess - remainingExcess from reserves, and minting full amount // to reserves pair.swap(buyTokens, 0, address(this), abi.encode(uniVars)); } } } } 

通過對代碼分析,buyReserveAndTransfer 首先會計算在 Uniswap 中用於兌換 yCRV 的 YAM 的數量,如果該數量少於 YAM 的鑄幣數量,則會將多餘的增發的 YAM 幣給 reserves 合約,這一步是通過 Uniswap 合約調用 rebase 合約的 uniswapV2Call 函數實現的,具體的代碼如下:

function uniswapV2Call( address sender, uint256 amount0, uint256 amount1, bytes memory data ) public { // enforce that it is coming from uniswap require(msg.sender == uniswap_pair, "bad msg.sender"); // enforce that this contract called uniswap require(sender == address(this), "bad origin"); (UniVars memory uniVars) = abi.decode(data, (UniVars)); YAMTokenInterface yam = YAMTokenInterface(yamAddress); if (uniVars.amountFromReserves > 0) { // transfer from reserves and mint to uniswap yam.transferFrom(reservesContract, uniswap_pair, uniVars.amountFromReserves); if (uniVars.amountFromReserves < uniVars.yamsToUni) { // if the amount from reserves > yamsToUni, we have fully paid for the yCRV tokens // thus this number would be 0 so no need to mint yam.mint(uniswap_pair, uniVars.yamsToUni.sub(uniVars.amountFromReserves)); } } else { // mint to uniswap yam.mint(uniswap_pair, uniVars.yamsToUni); } // mint unsold to mintAmount //SlowMist// 將多餘的 YAM 代幣分發給 reserves 合約 if (uniVars.mintToReserves > 0) { yam.mint(reservesContract, uniVars.mintToReserves); } // transfer reserve token to reserves if (isToken0) { SafeERC20.safeTransfer(IERC20(reserveToken), reservesContract, amount1); emit TreasuryIncreased(amount1, uniVars.yamsToUni, uniVars.amountFromReserves, uniVars.mintToReserves); } else { SafeERC20.safeTransfer(IERC20(reserveToken), reservesContract, amount0); emit TreasuryIncreased(amount0, uniVars.yamsToUni, uniVars.amountFromReserves, uniVars.mintToReserves); } } 

分析到這裡,一個完整的 rebase 流程就完成了,你可能看得很懵,我們用簡單的流程圖簡化下:

慢霧:DeFi 當紅項目 YAM 閃電折戟,一行代碼如何蒸發數億美元?

也就是說,每次的 rebase,如果有多餘的 YAM 代幣,這些代幣將會流到 reserves 合約中,那這和社區治理的關係是什麼呢?

通過分析項目代碼,發現治理相關的邏輯在 YAMGovernorAlpha.sol 中,其中發起提案的函數為 propose,具體代碼如下:

function propose( address[] memory targets, uint[] memory values, string[] memory signatures, bytes[] memory calldatas, string memory description ) public returns (uint256) { //SlowMist// 校驗提案發起者的票數佔比 require(yam.getPriorVotes(msg.sender, sub256(block.number, 1)) > proposalThreshold(), "GovernorAlpha::propose: proposer votes below proposal threshold"); require(targets.length == values.length && targets.length == signatures.length && targets.length == calldatas.length, "GovernorAlpha::propose: proposal function information arity mismatch"); require(targets.length != 0, "GovernorAlpha::propose: must provide actions"); require(targets.length <= proposalMaxOperations(), "GovernorAlpha::propose: too many actions"); uint256 latestProposalId = latestProposalIds[msg.sender]; if (latestProposalId != 0) { ProposalState proposersLatestProposalState = state(latestProposalId); require(proposersLatestProposalState != ProposalState.Active, "GovernorAlpha::propose: one live proposal per proposer, found an already active proposal"); require(proposersLatestProposalState != ProposalState.Pending, "GovernorAlpha::propose: one live proposal per proposer, found an already pending proposal"); } uint256 startBlock = add256(block.number, votingDelay()); uint256 endBlock = add256(startBlock, votingPeriod()); proposalCount++; Proposal memory newProposal = Proposal({ idproposalCount, proposermsg.sender, eta0, targetstargets, valuesvalues, signaturessignatures, calldatascalldatas, startBlockstartBlock, endBlockendBlock, forVotes0, againstVotes0, canceledfalse, executedfalse }); proposals[newProposal.id] = newProposal; latestProposalIds[newProposal.proposer] = newProposal.id; emit ProposalCreated( newProposal.id, msg.sender, targets, values, signatures, calldatas, startBlock, endBlock, description ); return newProposal.id; } 

通過分析代碼,可以發現在發起提案時,需要提案發起人擁有一定額度的票權利,這個值必須大於 proposalThreshold 計算得來的值,具體代碼如下:

function proposalThreshold() public view returns (uint256) { return SafeMath.div(yam.initSupply(), 100); } // 1% of YAM 

也就是說提案發起人的票權必須大於 initSupply 的 1% 才能發起提案。那 initSupply 受什麼影響呢?答案是 YAM 代幣的 mint 函數,代碼如下:

 * 

 function mint(address to, uint256 amount) external onlyMinter returns (bool) { _mint(to, amount); return true; } function_mint(address to, uint256 amount) internal { // increase totalSupply totalSupply = totalSupply.add(amount); // get underlying value uint256 yamValue = amount.mul(internalDecimals).div(yamsScalingFactor); // increase initSupply initSupply = initSupply.add(yamValue); // make sure the mint didnt push maxScalingFactor too low require(yamsScalingFactor <=_maxScalingFactor(), "max scaling factor too low"); // add balance _yamBalances[to] =_yamBalances[to].add(yamValue); // add delegates to the minter _moveDelegates(address(0),_delegates[to], yamValue); emit Mint(to, amount); } 

從代碼可知,mint 函數在每次鑄幣時都會更新 initSupply 的值,而這個值是根據 amount 的值來計算的,也就是鑄幣的數量。

現在,我們已經分析完所有的流程了,剩下的就是把所有的分析串起來,看看這次的漏洞對 YAM 產生了什麼影響,對上文的流程圖做拓展,變成下面這樣:

慢霧:DeFi 當紅項目 YAM 閃電折戟,一行代碼如何蒸發數億美元?

整個事件的分析如上圖,由於 rebase 的時候取的是上一次的 totalSupply 的值,所以計算錯誤的 totalSupply 的值並不會立即通過 mint 作用到 initSupply 上,所以在下一次 rebase 前,社區仍有機會挽回這個錯誤,減少損失。但是一旦下一次 rebase 執行,整個失誤將會變得無法挽回。

通過查詢 Etherscan 上 YAM 代幣合約的相關信息,可以看到 totalSupply 已經到了一個非常大的值,而 initSupply 還未受到影響。

慢霧:DeFi 當紅項目 YAM 閃電折戟,一行代碼如何蒸發數億美元?

慢霧:DeFi 當紅項目 YAM 閃電折戟,一行代碼如何蒸發數億美元?

前車之鑒

這次事件中官方已經給出了具體的修復方案,這裡不再贅述。這次的事件充分暴露了未經審計 DeFi 合約中隱藏的巨大風險,雖然 YAM 開發者已經在 Github 中表明 YAM 合約的很多代碼是參考了經過充分審計的 DeFi 項目如 Compound、Ampleforth、Synthetix 及 YEarn/YFI,但是仍無可避免地發生了意料之外的風險。

DeFi 項目 Yam Finance (YAM) 核心開發者 belmore 在推特上表示:「對不起,大家。我失敗了。謝謝你們今天的大力支持。我太難過了。」但是覆水已經難收,在此,慢霧安全團隊給出如下建議

1、由於 DeFi 合約的高度複雜性,任何 DeFi 項目都需在經過專業的安全團隊充分審計後再進行上線,降低合約發生意外的風險 。審計可聯繫慢霧安全團隊(team@slowmist.com

2、項目中去中心化治理應循序漸進,在項目開始階段,需要設置適當的許可權以防發生黑天鵝事件。

來源鏈接:mp.weixin.qq.com

冷萃財經原創,作者:Awing,轉載請註明出處:https://www.lccjd.top/2020/08/13/%e6%85%a2%e9%9b%be%ef%bc%9adefi-%e5%bd%93%e7%ba%a2%e9%a1%b9%e7%9b%ae-yam-%e9%97%aa%e7%94%b5%e6%8a%98%e6%88%9f%ef%bc%8c%e4%b8%80%e8%a1%8c%e4%bb%a3%e7%a0%81%e5%a6%82%e4%bd%95%e8%92%b8%e5%8f%91%e6%95%b0/?variant=zh-tw

0

掃一掃,分享到微信

猜你喜歡

文章評論

電子郵件地址不會被公開。 必填項已用*標註

後發表評論

    上一篇

    慢霧:DeFi 當紅項目 YAM 閃電折戟,一行代碼如何蒸發數億美元?

    下一篇

    DeFi 藝術周報:加密藝術正在成為一個新興產業

    微信公眾號

    微信公眾號