Reentrancy: The DAO에서 Euler Finance까지
스마트 컨트랙트 역사상 가장 많은 자금을 날린 단일 취약점 클래스, Reentrancy(재진입 공격). 2016년 The DAO 해킹(~$60M)부터 2021년 Cream Finance($130M), 2023년 Euler Finance($197M)까지 수십 억 달러를 앗아갔다.
Reentrancy란?
컨트랙트 A가 B를 외부 호출할 때, B가 다시 A를 호출하여 A의 상태가 아직 업데이트되지 않은 시점에 로직을 반복 실행하는 공격이다.
취약한 코드
contract VulnerableBank {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0);
// ⚠️ ETH 전송 후 잔액 업데이트 — 재진입 가능
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] = 0; // 이미 늦음!
}
}
The DAO (2016, ~$60M)
function splitDAO(uint _proposalID) returns (bool) {
if (!msg.sender.call.value(reward)()) throw; // 전송 먼저
totalSupply -= balances[msg.sender]; // 상태 업데이트 나중
balances[msg.sender] = 0;
}
→ ETH/ETC 하드포크의 원인이 됨
Cream Finance (2021, ~$130M)
AMP 토큰의 ERC1820 콜백이 Cross-contract Reentrancy 벡터로 사용됨. 담보로 잡힌 AMP를 재진입으로 반복 대출.
Euler Finance (2023, ~$197M)
donateToReserves()가 health factor를 검증하지 않아 청산 가능 상태를 인위적으로 생성. Flash Loan과 조합.
방어 1: CEI 패턴
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0);
balances[msg.sender] = 0; // Effects 먼저
(bool success, ) = msg.sender.call{value: amount}(""); // Interactions 나중
require(success);
}
방어 2: ReentrancyGuard
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SafeBank is ReentrancyGuard {
function withdraw() external nonReentrant {
uint256 amount = balances[msg.sender];
require(amount > 0);
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
}
방어 3: Pull Payment
ETH를 직접 push하지 않고, 수신자가 직접 pull하게 만든다.
체크리스트
- [ ] 외부 호출 전에 상태 변경 완료? (CEI)
- [ ] nonReentrant modifier 적용?
- [ ] ERC777/ERC1155 콜백 토큰 검토?
- [ ] Cross-contract Reentrancy 고려?
ContractScan으로 탐지하기
Slither + Semgrep + AI 삼중 레이어로 Reentrancy 패턴을 자동 탐지합니다.
→ ContractScan 무료 스캔