通縮項目在業務設計的時候一定要考慮到與 pair 交互的情況,自身的通縮機制是否會對 pair 產生影響。
封面: Photo by Dynamic Wang on Unsplash
近期 Beosin 安全團隊研究發現,通縮代幣引起的安全事件依然頻發,造成眾多項目方資金的損失,因此,Beosin 安全團隊準備了這篇詳解通縮代幣的文章,與大家分享。
本文將對通縮代幣與 pair 結合過程中容易出現的問題以及歷史發生的真實通縮代幣安全事件兩個方面進行介紹,通過本文,我們將徹底搞清楚通縮代幣是什麼意思以及通縮代幣發生安全問題所涉及的原理,使我們在之後的項目中避坑。
1 通縮代幣是哪種類型的幣?
通縮代幣是一種在交易過程中會進行相關比例銷毀的代幣,這是一種很好的激勵用戶持有代幣的方式。
在代幣交易過程中,會扣除部分代幣用於手續費、獎勵以及銷毀,而隨著代幣的銷毀,總供應量便會不斷減少,就能使得用戶持有代幣所佔比例增加,從而使得用戶更願意持有代幣來被動獲取更高的收益。
看似完美的金融方案,但在代碼實現上並不像預想的那麼完美。代碼中存在銷毀過程,此過程將繞過 swap 過程直接修改地址餘額,這種情況與 pair 相結合,便會出現一些意想不到的問題。
2 通縮代幣存在哪些問題?
(1)添加流動性問題
通縮代幣在轉賬時會收取一定比例的手續費給當前合約,並在手續費達到某個閾值(當前代幣數量大於等於合約設置的某個變量)時會調用 pair 合約進行 swap、addLiquidity 或 sync 等操作。
如果在通縮代幣交易過程中,沒有排除 to 地址等於 pair 合約地址,並且該通縮代幣在 pair 中為 TokenB 時,那麼在進行 TokenA 與 TokenB 添加流動性的操作中可能導致失敗。
為什麼會出現交易失敗的問題呢?添加流動性是將 TokenA 與 TokenB 兩種代幣打入 pair 合約,然後調用 pair 合約的 mint 函數(下方詳情),該函數會根據本合約的當前餘額與儲備量的差值來判斷用戶傳入了多少代幣。
用戶將 TokenA 的代幣發送至 pair 後,進行 TokenB 代幣轉賬,當收取的手續費正好達到上述的閾值時,代幣合約調用 pair 的 swap、mint 或 sync 函數,這幾個函數都會調用 pair 的_update 函數,從而將用戶最開始發送至 pair 的 TokenA 更新為 reserve。
最後,用戶再調用 mint 函數,會導致 TokenA 的 balance 和 reserve 是相等的,結果將導致該筆交易失敗。
Mint 函數代碼如下:
function mint(address to) external lock returns (uint liquidity){ (uint112 _reserve0, uint112 _reserve1,) = getReserves(); //获取储备量 uint balance0 = IERC20(token0).balanceOf(address(this));//获取当前合约 TokenA 余额 uint balance1 = IERC20(token1).balanceOf(address(this));//获取当前合约 TokenB 余额 uint amount0 = balance0.sub(_reserve0);//差值计算结果为 0 uint amount1 = balance1.sub(_reserve1); bool feeOn = _mintFee(_reserve0, _reserve1); uint _totalSupply = totalSupply; if (_totalSupply == 0) { liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY); _mint(address(0), MINIMUM_LIQUIDITY); } else { liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1); } require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');//此处会导致交易失败 _mint(to, liquidity); _update(balance0, balance1, _reserve0, _reserve1); if (feeOn) kLast = uint(reserve0).mul(reserve1); emit Mint(msg.sender, amount0, amount1);}
整個調用過程如下:
(2)Skim 問題
Pair 合約擁有一個 skim 函數(下方詳情),該函數會將 pair 合約中超出儲備量的代幣發送到調用者指定地址,數量計算方式是根據 pair 合約所擁有的代幣數量與儲備量之間的差值來實現的,這本身是一個平衡 pair 供應量的功能,但遇到其中一個代幣為通縮代幣,便可能出現問題。
通縮代幣在交易過程中會扣取一部分的費用,那麼如果在 skim 函數中代幣轉賬過程扣取的費用是由 from“買單”,會出現什麼問題呢?
此時扣取的費用將會是 pair 的供應量,這樣就能提前向 pair 中轉入代幣,通過不斷的 skim 函數與 sync 函數消耗掉 pair 的供應量,使得該種代幣在 pair 中的價格不斷飆升,最終使用少部分該通縮代幣就能兌換出大量的另一種代幣(一般為 usdt、eth 等價值幣)。
Skim 函數代碼如下:
function skim(address to) external lock {address _token0 = token0; address _token1 = token1; _safeTransfer(_token0,to,IERC20(_token0).balanceOf(address(this)).sub(reserve0)); _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));}
整個調用過程如下:
(3)銷毀問題
該問題主要出現在使用 “映射” 機制的通縮代幣中,這種代幣的機制是存在兩種代幣餘額存儲變量,分別為 tOwned 和 rOwned,而 tOwned 存儲的是實際代幣數量,rOwned 存儲的是通過 currentRate 變量放大映射之後的值。
rOwned 的作用是什麼呢?在文章開始說過,通縮代幣能激勵用戶持有代幣,這種激勵目的使用的方式便是對交易者扣除 rOwned 值,同時扣除 rTotal,這樣其他用戶 rOwned 所佔 rTotal 的比例就會被動增加,實現被動收益。(rOwned 與 rTotal 可理解為用戶的股份以及總股份)
用戶查詢餘額的方式有兩種情況,一種是除外地址,直接返回 tOwned 的值,另一種是非除外地址,返回 rOwned/currentRate,而 currentRate 計算方式為 rTotal/tTotal。如果有辦法使得 rTotal 減小,那麼用戶查詢出的實際餘額將變大,而如果 pair 查詢餘額變大,則可以通過 skim 函數將多餘的代幣轉移出去。
而該類通縮代幣存在一個 deliver() 函數,非除外地址可調用,該函數會將調用者的 rOwned 銷毀,並銷毀相同數量的_rTotal,使得所有非除外地址的餘額查詢增加,pair 如果非除外的話,便可使用上述方式套利攻擊。
3 通縮代幣相關安全事件剖析
(1)AES 安全事件
北京時間 2023 年 1 月 30 日,Beosin 旗下 Beosin EagleEye 安全風險監控、預警與阻斷平台監測到,AES 遭受到黑客攻擊,該項目便存在上述的 Skim 問題。
AES-USDT pair 合約有一個 skim 函數,該函數可以強制平衡 pair 的供應量,將多餘資金發送給指定地址。
攻擊者在本次攻擊過程中,首先向 pair 裡面直接轉入了部分 AES 代幣,導致供應量不平衡,從而攻擊者調用 skim 函數時,會將多餘的這部分代幣轉到攻擊者指定地址,而攻擊者在此處指定了 pair 合約為接收地址,使得多餘的 AES 又發送到了 pair 合約,導致強制平衡之後 pair 合約依然處於不平衡狀態,攻擊者便可重複調用強制平衡函數,而 AES 發送過程會調用到 AES 合約的 transfer 函數,如下圖。
另外一點,當調用 AES 代幣合約的 transfer 函數時,若發送者為合約設置的 pair 合約時,會將一部分費用記錄在 swapFeeTotal 之中(如上圖過程),在最後的時候可以統一調用 distributeFee 函數(如下圖)將 swapFeeTotal 記錄的費用從 pair 中轉出,這里相比上述的過程,攻擊者可以不用做 sync 函數調用操作,而是在最後將費用轉移出去之後調用一次 sync 函數即可。
攻擊者經過反复的強制平衡操作,費用記錄變得異常大,基本接近 pair 的總餘額,最後攻擊者調用 distributeFee 函數將 pair 裡面的 AES 轉出,pair 的 AES 餘額變得非常少,導致攻擊者利用少量 AES 兌換了大量的 USDT。
(2)BevoToken 安全事件
北京時間 2023 年 1 月 30 日,Beosin 旗下 Beosin EagleEye 安全風險監控、預警與阻斷平台監測到,BevoToken 遭受到閃電貸攻擊,該項目便是上面所說的 “映射” 機制通縮代幣。
由於 BevoToken 合約的 balanceOf 函數(如下圖)並非 ERC20 標準的函數,該函數在經過一些計算處理後再返回餘額,而轉賬或其他操作可能使前後計算返回的餘額不一致,當攻擊者在 swap 操作前後可憑藉這個問題來操控 pair 合約的餘額,從而 skim 出多餘的代幣。
攻擊者首先在 pancake 貸出 192.5 個 BNB,之後換成約 302,877 個 BEVO 代幣,再調用被攻擊合約的 deliver 函數(如下圖),此時_rTotal 的值減小,_rTotal 的值減小會導致_getRate 中計算的值偏小,此時 balanceOf 返回的餘額則會偏大,導致攻擊者能 skim 出多餘的 BEVO。
之後,攻擊者再將 skim 出的代幣進行 deliver,此時_rTotal 的值已經很小了,在進行_getRate 計算時,會減去除外地址的 rOwned(如下圖),此值固定且被攻擊者在之前通過 burn 異常放大的,在最開始_rTotal 正常的時候,減去該值對結果的影響不大,但是現在_rTotal 被攻擊者操控得異常小,再減去這個異常放大的固定值後,對結果產生了巨大的影響,第一次 deliver 導致 pair 計算結果偏大 3 倍,而第二次 deliver 之後,pair 計算結果則偏大了數百倍,這也是為什麼攻擊者獲得的代幣要比自己銷毀的代幣多得多的原因。
4 Beosin 總結
通縮項目在業務設計的時候一定要考慮到與 pair 交互的情況,自身的通縮機制是否會對 pair 產生影響。我們也建議相關項目上線前尋找專業的安全審計機構進行全面的代碼以及業務的安全審計工作。
免責聲明:作為區塊鏈信息平台,本站所發布文章僅代表作者及嘉賓個人觀點,與 Web3Caff 立場無關。文章內的信息僅供參考,均不構成任何投資建議及要約,並請您遵守所在國家或地區的相關法律法規。