Flash Loan 공격 완전 해부
Flash Loan은 DeFi의 혁신이자 최대 공격 벡터다. 담보 없이 수백만 달러를 빌려 단일 트랜잭션 내에서 가격을 조작하고, 차익을 챙기고, 대출을 상환한다. 공격자의 초기 자본은 가스비뿐.
Flash Loan이란?
같은 트랜잭션 내에서 빌리고 갚는 무담보 대출. 상환하지 않으면 전체 트랜잭션이 revert된다. Aave, dYdX, Uniswap V2/V3에서 제공.
// Aave V3 Flash Loan 인터페이스
function flashLoan(
address receiverAddress,
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata interestRateModes,
address onBehalfOf,
bytes calldata params,
uint16 referralCode
) external;
공격 패턴: Price Oracle Manipulation
1. Flash Loan으로 대량 토큰 A 차입
2. DEX에서 토큰 A를 대량 매도 → 토큰 A 가격 급락
3. 타겟 프로토콜이 DEX spot price를 oracle로 사용
4. 저렴해진 가격으로 담보 설정 또는 청산 실행
5. 가격 복구 후 차익 실현
6. Flash Loan 상환
사례 1: bZx (2020, ~$1M)
DeFi Flash Loan 공격의 시초. Uniswap에서 sUSD 가격을 조작하여 과대평가된 담보로 대출.
공격 흐름:
1. dYdX에서 10,000 ETH Flash Loan
2. 5,500 ETH를 Compound에 담보 → 112 WBTC 대출
3. 나머지 ETH로 Fulcrum에서 ETH short position
4. Uniswap/Kyber에서 가격 조작
5. Short position에서 이익 실현
6. Flash Loan 상환, 차익 획득
사례 2: PancakeBunny (2021, ~$45M)
BSC 기반 프로토콜. Pancakeswap의 spot price를 oracle로 사용하여 BUNNY 토큰 가격 조작.
근본 원인: priceCalculator가 단일 블록의 AMM spot price에 의존. Flash Loan으로 순간적으로 가격을 왜곡하여 과도한 BUNNY 민팅.
사례 3: Euler Finance (2023, ~$197M)
Flash Loan + donateToReserves() 조합. Health factor 검증 부재로 인위적 청산 상태 생성.
공격 흐름:
1. Aave에서 DAI Flash Loan
2. Euler에 DAI 예치 → eDAI 민팅
3. eDAI로 추가 DAI 대출 (레버리지)
4. donateToReserves()로 eDAI를 reserve에 기부 → 담보 부족 상태
5. 별도 계정으로 자신의 포지션 청산 → 할인된 가격에 담보 획득
6. Flash Loan 상환, 차익 획득
방어 1: TWAP Oracle 사용
단일 블록의 spot price 대신 Time-Weighted Average Price를 사용.
// Uniswap V3 TWAP Oracle
function consult(address pool, uint32 secondsAgo) external view returns (int24 arithmeticMeanTick) {
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = secondsAgo;
secondsAgos[1] = 0;
(int56[] memory tickCumulatives, ) = IUniswapV3Pool(pool).observe(secondsAgos);
arithmeticMeanTick = int24(
(tickCumulatives[1] - tickCumulatives[0]) / int56(int32(secondsAgo))
);
}
방어 2: Chainlink Oracle
온체인 DEX 가격 대신 오프체인 데이터 피드 사용. Flash Loan으로 조작 불가.
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
function getPrice() external view returns (uint256) {
(, int256 price, , uint256 updatedAt, ) = priceFeed.latestRoundData();
require(block.timestamp - updatedAt < 3600, "Stale price");
return uint256(price);
}
방어 3: 단일 트랜잭션 제한
mapping(address => uint256) private _lastActionBlock;
modifier noSameBlockAction() {
require(_lastActionBlock[msg.sender] != block.number, "Same block");
_lastActionBlock[msg.sender] = block.number;
_;
}
방어 4: Health Factor 검증
상태 변경 함수 실행 후 반드시 health factor를 재검증.
function donateToReserves(uint256 amount) external {
// ... donate logic ...
require(getHealthFactor(msg.sender) >= MIN_HEALTH_FACTOR, "Unhealthy");
}
체크리스트
- [ ] Price oracle이 단일 블록 spot price에 의존하지 않는가?
- [ ] TWAP 또는 Chainlink 등 조작 저항성 oracle 사용?
- [ ] 동일 블록 내 가격 의존 작업 제한?
- [ ] 상태 변경 후 health factor 재검증?
- [ ] Flash Loan callback에서 의도하지 않은 상태 변경 없는가?
ContractScan으로 탐지하기
Semgrep의 single-transaction-price-manipulation, unchecked-oracle-price 룰과 AI 분석으로 Flash Loan 취약 패턴을 탐지합니다.
→ ContractScan 무료 스캔