技術的な観点から解析:なぜデフレメカニズムのトークンは攻撃を受けやすいのか

始新世研究
2023-03-08 23:45:10
コレクション
本文では、トークンが攻撃を受ける原因について議論し分析し、相応の防御策を提示します。

著者:Eocene Research

概要

最近、デフレメカニズムを持つトークンは攻撃を受けることが多くなっています。本稿では、トークンが攻撃を受ける理由について議論し、分析し、相応の防御策を提案します。

トークンにデフレメカニズムを実装する方法は通常2つあり、一つはバーニングメカニズム、もう一つはリフレクションメカニズムです。以下では、これら2つの実装方法に存在する可能性のある問題を分析します。

バーニングメカニズム

通常、バーニングメカニズムを持つトークンはその _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という関数があり、流動性プール内の2つのトークンの残高と準備金の差を呼び出し元に移転し、残高と準備金をバランスさせます:

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));

}

この時、送信者は流動性プールに変わり、_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)を盗みます。

リフレクションメカニズム

リフレクションメカニズムでは、ユーザーは毎回の取引で手数料を支払い、トークンを保有するユーザーに報酬を与えますが、転送はトリガーされず、単に係数が変更されるだけです。

このメカニズムでは、ユーザーには2種類のトークン数量があり、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; // ガス節約

address _token1 = token1; // ガス節約

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):

防御策

バーニングメカニズムとリフレクションメカニズムのトークンに対する攻撃手法を解読することで、攻撃者が攻撃する核心点は流動性プールの価格を操作することにあることが明らかになります。したがって、流動性プールのアドレスをホワイトリストに追加し、トークンのバーニングに関与せず、トークンのリフレクションメカニズムに参加しないようにすることで、このような攻撃を回避できます。

まとめ

本稿では、デフレメカニズムトークンの2つの実装メカニズムとそれに対する攻撃手段を分析し、最終的に相応の解決策を提案しました。コントラクトを作成する際、プロジェクト側はトークンと分散型取引所の結合を考慮し、このような攻撃を避ける必要があります。

ChainCatcherは、広大な読者の皆様に対し、ブロックチェーンを理性的に見るよう呼びかけ、リスク意識を向上させ、各種仮想トークンの発行や投機に注意することを提唱します。当サイト内の全てのコンテンツは市場情報や関係者の見解であり、何らかの投資助言として扱われるものではありません。万が一不適切な内容が含まれていた場合は「通報」することができます。私たちは迅速に対処いたします。
banner
チェーンキャッチャー イノベーターとともにWeb3の世界を構築する