技術的な観点から解析:なぜデフレメカニズムのトークンは攻撃を受けやすいのか
著者: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つの実装メカニズムとそれに対する攻撃手段を分析し、最終的に相応の解決策を提案しました。コントラクトを作成する際、プロジェクト側はトークンと分散型取引所の結合を考慮し、このような攻撃を避ける必要があります。