From a technical perspective: Why tokens with deflationary mechanisms are vulnerable to attacks
Author: Eocene Research
Overview
Tokens with deflationary mechanisms on the blockchain have recently been frequently attacked. This article will discuss and analyze the reasons why token tokens are attacked and provide corresponding defense solutions.
There are usually two ways to implement a deflationary mechanism in tokens: one is the burn mechanism, and the other is the reflection mechanism. Below, we will analyze the potential issues with these two implementation methods.
Burn Mechanism
Typically, tokens with a burn mechanism will implement the burning logic in their _transfer
function. Sometimes, the sender may bear the transaction fee. In this case, the number of tokens received by the recipient does not change, but the sender needs to pay more tokens because they have to bear the transaction fee. Here is a simple example:
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;
}
Then we discuss the potential risks in this case.
If we look solely at the token contract, we will find that this writing actually has no issues, but there are many complex situations in the blockchain that require us to consider many aspects.
Typically, to give the token a price, the project team will add liquidity for the token on decentralized exchanges such as Uniswap and Pancakeswap.
In Uniswap, there is a function called skim
, which transfers the difference between the balances and reserves of the two tokens in the liquidity pool to the caller to balance the balances and reserves:
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));
}
At this point, the sender becomes the liquidity pool, and when calling _transfer
, some tokens in the liquidity pool will be partially burned, causing the token price to rise partially.
Attackers exploit this feature by directly transferring tokens into the liquidity pool, then calling the skim
function to withdraw, and then repeating this operation multiple times, resulting in a large number of tokens being burned in the liquidity pool, and the price skyrocketing, ultimately selling the tokens for profit.
A real attack case is 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;
}
In the _transfer
function of the WDOGE contract, when block.timestamp > closingTime
, it enters the else loop. In line 21 of the code, the transfer amount is deducted from the sender's balance, and in line 31 of the code, the sender is burned the amount of tokensToBurn
. The attacker exploits this fee mechanism to steal all valuable tokens (WBNB) from the liquidity pool through the above attack method.
Reflection Mechanism
In the reflection mechanism, users are charged a fee for each transaction, which is used to reward users holding tokens, but it does not trigger a transfer; it simply modifies a coefficient.
In this mechanism, users have two types of token amounts: tAmount
and rAmount
. tAmount
is the actual token amount, and rAmount
is the reflected token amount, with a ratio of tTotal / rTotal
. A typical code implementation is as follows:
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);
}
Tokens with a reflection mechanism generally have a function called deliver
, which will burn the caller's tokens, reducing the value of rTotal
, thus increasing the ratio, and the reflected token amounts for other users will also increase:
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);
}
The attacker notices this function and uses it to attack the corresponding Uniswap liquidity pool.
So how can they exploit it? Again, starting from the Uniswap skim
function:
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));
}
In Uniswap, reserve
is the reserve fund, which is different from token.balanceOf(address(this))
.
The attacker first calls the deliver
function to burn their tokens, causing the value of rTotal
to decrease, which in turn increases the ratio, so the value of the reflected tokens will also increase, and token.balanceOf(address(this))
will also correspondingly increase, creating a discrepancy with the value of reserve
.
Therefore, the attacker can profit by calling the skim
function to withdraw tokens equal to the difference between the two.
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
A real attack case is BEVO NFT Art Token (BEVO):
When there is a burn
function in the token contract, there exists another similar attack method:
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);
}
When a user calls the burn
function, their tokens will be burned, and the value of tTotal
will decrease, so the ratio will decrease, and the corresponding reflected token amount will also decrease, causing the number of tokens in the liquidity pool to decrease, thus increasing the token price.
Attackers exploit this feature by repeatedly calling the burn
function to reduce the value of tTotal
, then calling the liquidity pool's sync
function to synchronize reserves and balances. Finally, the number of tokens in the liquidity pool decreases significantly, and the price skyrockets. The attacker then sells the tokens for profit.
Attacker: token.burn
tTotal
: decrease
rate
: decrease
tokenFromReflection
: decrease
balanceOf
: decrease
Attacker: pair.sync
token.balanceOf(address(this))
> reserve
token.transfer
A real attack case is Sheep Token (SHEEP):
Defense Solutions
By interpreting the attack methods against tokens with burn and reflection mechanisms, it is not difficult to find that the core point of the attackers' attacks is to manipulate the price of the liquidity pool. Therefore, adding the liquidity pool address to the whitelist, not involving the destruction of tokens, and not participating in the token reflection mechanism can avoid such attacks.
Conclusion
This article analyzes the two implementation mechanisms of deflationary mechanism tokens and the attack methods against these two mechanisms, and finally provides corresponding solutions. When writing contracts, project teams must consider the integration of tokens with decentralized exchanges to avoid such attacks.