從技術角度解析:為什麼通縮機制的代幣易受攻擊

EoceneResearch
2023-03-08 23:45:10
收藏
本文將討論並分析代幣令牌受到攻擊的原因,並給出相應的防禦方案。

作者:Eocene Research

概述

在區塊鏈上具有通縮機制的代幣最近經常受到攻擊。本文將討論並分析代幣令牌受到攻擊的原因,並給出相應的防禦方案。

在代幣中實現通縮機制通常有兩種方式,一種是燃燒機制,另一種是反射機制。下面我們將分析這兩種實現方式可能存在的問題。

燃燒機制

通常,具有燃燒機制的代幣將在其 _transfer 函數中實現燃燒的邏輯。有時候會存在發送者承擔手續費的情況。在這種情況下,接收方收到的代幣數量不會發生變化,但發送方需要支付更多代幣,因為其需要承擔手續費。下面是一個簡單的例子:

function _transfer(address sender, address recipient, uint256 amount) internal virtual returns (bool) {

require(_balances[sender] >= amount, "ERC20: transfer amount exceeds balance");

require(sender != address(0), "ERC20: transfer from the zero address");

require(recipient != address(0), "ERC20: transfer to the zero address");

burnFee = amount * burnFeeRate;

_balances[sender] -= amount;

_burn(sender, burnFee);

_balances[recipient] += amount;

}

然後我們討論這種情況下可能存在的風險。

如果單看代幣合約,我們會發現這種寫法其實沒有什麼問題,但是區塊鏈中有很多複雜的情況,需要我們考慮很多方面。

通常,為了讓代幣有價格,項目方會在 Uniswap、Pancakeswap 等去中心化交易所為代幣添加流動性。

其中,在 Uniswap 中,有一個函數 skim,它會將流動性池中兩種代幣的餘額和儲備金的差值轉移給調用方,以平衡餘額和儲備金:

function skim(address to) external lock {

address _token0 = token0; // gas savings

address _token1 = token1; // gas savings

safeTransfer(token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));

safeTransfer(token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));

}

此時發送方變成了流動性池,當調用 _transfer 時,流動性池中的代幣將被部分銷燬,導致代幣價格部分上漲。

攻擊者利用此特性將代幣直接轉入流動性池中,然後調用 skim 函數轉出,然後多次重複此操作,導致流動性池中大量代幣被燃燒,價格也隨之飆升,最後賣出代幣獲利。

一個真實的攻擊案例,winner doge (WDOGE):

function _transfer(address sender, address recipient, uint256 amount) internal virtual returns (bool) {

require(_balances[sender].amount >= amount, "ERC20: transfer amount exceeds balance");

require(sender != address(0), "ERC20: transfer from the zero address");

require(recipient != address(0), "ERC20: transfer to the zero address");

if(block.timestamp >= openingTime \&\& block.timestamp \<= closingTime)

{

_balances[sender].amount -= amount;

_balances[recipient].amount += amount;

emit Transfer(sender, recipient, amount);

}

else

{

uint256 onePercent = findOnePercent(amount);

uint256 tokensToBurn = onePercent *4;

uint256 tokensToRedistribute = onePercent * 4;

uint256 toFeeWallet = onePercent*1;

uint256 todev = onePercent* 1;

uint256 tokensToTransfer = amount - tokensToBurn - tokensToRedistribute - toFeeWallet-todev;

_balances[sender].amount -= amount;

_balances[recipient].amount += tokensToTransfer;

_balances[feeWallet].amount += toFeeWallet;

_balances[dev].amount += todev;

if (!_balances[recipient].exists){

_balanceOwners.push(recipient);

_balances[recipient].exists = true;

}

redistribute(sender, tokensToRedistribute);

_burn(sender, tokensToBurn);

emit Transfer(sender, recipient, tokensToTransfer);

}

return true;

}

在 WDOGE 合約的_transfer 函數中,當 block.timestamp > closingTime 時,進入 else 循環。在代碼第 21 行中,轉帳金額從發送方的餘額中扣除,在代碼第 31 行中,發送方又被燃燒了 tokensToBurn 數量的代幣。攻擊者利用這種手續費的機制,通過上述的攻擊方式竊取流動性池中的所有價值代幣 (WBNB)。

反射機制

在反射機制中,用戶每次交易都會收取手續費,用於獎勵持有代幣的用戶,但不會觸發轉帳,只是單純修改一個係數。

在這個機制中,用戶有兩種類型的代幣數量,tAmount 和 rAmount。 tAmount 為實際代幣數量,rAmount 為反映後的代幣數量,比率為 tTotal / rTotal,一般的代碼實現如下:

function balanceOf(address account) public view override returns (uint256) {

if (_isExcluded[account]) return _tOwned[account];

return tokenFromReflection(_rOwned[account]);

}

function tokenFromReflection(uint256 rAmount) public view returns(uint256) {

require(rAmount \<= _rTotal, "Amount must be less than total reflections");

uint256 currentRate = _getRate();

return rAmount.div(currentRate);

}

function _getRate() private view returns(uint256) {

(uint256 rSupply, uint256 tSupply) = _getCurrentSupply();

return rSupply.div(tSupply);

}

反射機制的代幣中一般有一個叫做 deliver 的函數,會銷燬調用者的代幣,降低 rTotal 的值,所以比率會增加,其他用戶反射後的代幣數量也會增加:

function deliver(uint256 tAmount) public {

address sender = _msgSender();

require(!_isExcluded[sender], "Excluded addresses cannot call this function");

(uint256 rAmount,,,,,) = _getValues(tAmount);

_rOwned[sender] = _rOwned[sender].sub(rAmount);

_rTotal = _rTotal.sub(rAmount);

_tFeeTotal = _tFeeTotal.add(tAmount);

}

攻擊者注意到這個函數,並用它來攻擊相應的 Uniswap 的流動性池。

那他該如何進行利用呢?同樣從 Uniswap 的 skim 函數開始:

function skim(address to) external lock {

address _token0 = token0; // gas savings

address _token1 = token1; // gas savings

safeTransfer(token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));

safeTransfer(token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));

}

Uniswap 中 reserve 是儲備金,與 token.balanceOf(address(this)) 不同。

攻擊者先調用 deliver 函數銷燬自己的代幣,導致 rTotal 的值減少,比率隨之增加,所以反射後的代幣的值也會增加,token.balanceOf(address(this)) 也會相應變大,與 reserve 的值出現了差距。

因此,攻擊者可以通過調用 skim 函數轉出數量為兩者之間差值的代幣從而進行獲利。

Attacker: token.deliver

rtotal: decrease

rate: increase

tokenFromReflection: increase

balanceOf: increase -> token.balanceOf(address(this)) > reserve

Attacker: pair.skim

token.balanceOf(address(this)) > reserve

token.transfer

一個真實的攻擊案例,BEVO NFT Art Token (BEVO):

而當代幣合約中存在 burn 函數時,存在了另外一種相似的攻擊手法:

function burn(uint256 _value) public{

_burn(msg.sender, _value);

}

function _burn(address _who, uint256 _value) internal {

require(_value \<= _rOwned[_who]);

rOwned[who] = rOwned[who].sub(_value);

_tTotal = _tTotal.sub(_value);

emit Transfer(_who, address(0), _value);

}

當用戶調用 burn 函數時,自己的代幣會被銷燬,同時 tTotal 的值會減少,所以比率會降低,對應的反射後的代幣數量也會減少,所以在此時流動性池的代幣的數量也會減少,從而代幣的價格會上漲。

攻擊者利用這個特性通過多次調用 burn 函數來減少 tTotal 的值,然後調用流動性池的 sync 函數同步 reserve 和 balances。最後,流動性池中的代幣大幅減少,價格飆升。然後攻擊者出售代幣以獲取利潤。

Attacker: token.burn

tTotal: decrease

rate: decrease

tokenFromReflection: decrease

balanceOf: decrease

Attacker: pair.sync

token.balanceOf(address(this)) > reserve

token.transfer

一個真實的攻擊案例,Sheep Token (SHEEP):

防禦方案

通過解讀針對燃燒機制和反射機制代幣的攻擊手法,不難發現攻擊者攻擊的核心點是操縱流動性池的價格,因此將流動性池的地址加入白名單,不涉及代幣的銷燬,不參與代幣的反射機制,可以避免此類攻擊。

總結

本文分析了通縮機制代幣的兩種實現機制以及針對這兩種機制的攻擊手段,最後給出了相應的解決方案。在編寫合約時,項目方必須考慮代幣與去中心化交易所結合的情況,以避免此類攻擊。

鏈捕手ChainCatcher提醒,請廣大讀者理性看待區塊鏈,切實提高風險意識,警惕各類虛擬代幣發行與炒作,站內所有內容僅係市場信息或相關方觀點,不構成任何形式投資建議。如發現站內內容含敏感信息,可點擊“舉報”,我們會及時處理。
banner
ChainCatcher 與創新者共建Web3世界