컨트랙트의 상태가 업데이트 되기 전, 함수를 다시 호출(재진입)하여 공격하는 기법
다음과 같은 기능을 갖고있는 컨트랙트는 reentrancy attack 에 취약합니다.

다음은 Reentrancy 에 취약한 컨트랙트와 이를 공격하는 컨트랙트 코드 예시입니다.
contract vulnerableContract{
mapping(address => uint256) public balances;
function withdraw() public{
require(balances[msg.sender] > 0);
(bool success, ) = msg.sender.call{value: balances[msg.sender]}("");
require(success);
balances[msg.sender] = 0;
}
}
contract maliciousContract{
IVulnerableContract public vCa = "0xsomeAddress..";
function exploit() external {
vCa.withdraw();
}
fallback() external payable {
if(address(vCa).balance >= vCa.balances(address(this))){
vCa.withdraw();
}
}
}
위의 두 컨트랙트는 다음과 같은 흐름으로 exploit이 됩니다.

contract safeContract{
mapping(address => uint256) public balances;
function withdraw() public{
require(balances[msg.sender] > 0);
uint256 transferAmount = balnaces[msg.sender];
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: transferAmount}("");
require(success);
}
}
상태(msg.sender 의 balance) 업데이트 → 자산 전송 의 순서로 코드가 실행되면 재진입을 하더라도 잔액을 확인하는 부분에서 오류가 발생하기 때문에 재진입 공격에 실패하게 됩니다.
contract safeContract{
mapping(address => uint256) public balances;
bool mutex = false;
function withdraw() public{
require(!mutex, "Mutex is locked");
require(balances[msg.sender] > 0, "Insufficient balance");
mutex = true;
(bool success, ) = msg.sender.call{value: balances[msg.sender]}("");
require(success, "tx is failed");
balances[msg.sender] = 0;
mutex = false;
}
}
재진입을 하더라도 mutex가 잠겨있는 상태이기 때문에 tx가 실패하게 됩니다.
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract safeContract {
mapping(address => uint256) public balances;
function withdraw() public nonReentrant{
require(balances[msg.sender] > 0, "Insufficient balance");
(bool success, ) = msg.sender.call{value: balances[msg.sender]}("");
require(success, "tx is failed");
balances[msg.sender] = 0;
}
}
mutex와 동일한 원리입니다. openzeppelin 에서 표준화한 코드를 사용합니다.