무료 미리 보기10분 분량
재진입: 아직도 통하는 가장 오래된 수법
자기 기록을 갱신하기 전에 돈을 먼저 보내는 컨트랙트는, 보내는 중간에 그 돈을 다시 요구받을 수 있어요. 그게 재진입(reentrancy)이고, Ethereum 역사상 첫 번째 대형 참사의 원인이었고, 지금도 피해자를 찾아내고 있어요.
작동 방식
- 컨트랙트가 예치자들의 잔고를 보관하고 출금 함수를 제공해요.
- 출금 함수는 자금을 먼저 보내고, 그다음에 예치자의 잔고를 0으로 만들어요.
- 다른 컨트랙트로 자금을 보내면 그 컨트랙트의 코드로 제어권이 넘어가요.
- 그 코드가 출금을 다시 호출해요. 잔고가 아직 0이 되지 않았으니 확인을 두 번째로도 통과해요.
- 풀이 빌 때까지 이 반복이 계속돼요.
해법은 확인, 변경, 상호작용이라고 불리는 순서 규칙이에요. 먼저 검증하고, 두 번째로 내 상태를 갱신하고, 마지막에 다른 쪽과 이야기하세요. 첫 호출이 열려 있는 동안 두 번째 진입을 거부하는 잠금 장치인 재진입 가드는 그 위에 더하는 안전장치예요.
어디에서 일어났을까요
- The DAO, 2016년 6월. 재진입 결함으로 Ethereum 위에 만들어진 투자 펀드에서 약 360만 ETH가 빠져나갔어요. 그 대응이 체인을 갈라놨어요. 다수는 도난을 되돌리려고 포크했고, 되돌리기를 거부한 체인은 오늘날 Ethereum Classic으로 이어지고 있어요.
- Curve Finance, 2023년 7월. 결함은 Curve 자체 로직이 아니라 Vyper 컴파일러의 특정 버전들에 있었고, 거기서는 재진입 가드가 적힌 대로 작동하지 않았어요. 여러 풀이 비워졌고 손실은 약 7000만 달러로 보고됐어요. 감사받은 코드도 그걸 컴파일하는 도구 때문에 무너질 수 있어요.
왜 살아남았을까요
- 어떤 외부 호출이든 제어권을 넘겨줄 수 있어요. 토큰 전송 훅, 콜백, 그리고 받는 쪽에 알려 주는 최신 토큰 표준들까지요.
- 읽기 전용 재진입은 더 미묘해요. 공격자가 상태가 절반만 갱신된 동안 조회 함수로 재진입하면, 다른 프로토콜이 잘못된 가격이나 잔고를 읽고 그걸로 움직여요.
- 함수 간 재진입은 같은 상태를 공유하는 다른 함수를 쓰기 때문에, 한 함수에만 가드를 걸어 두는 건 충분하지 않아요.
실제로 막아 주는 것
- 확인, 변경, 상호작용을 뻔한 출금 함수뿐 아니라 어디에나 적용하기요.
- 외부 호출을 하는 모든 상태 변경 진입점에 재진입 가드를 걸기요.
- 모든 토큰을 적대적인 코드로 취급하기요. 토큰 컨트랙트는 전송 중에 자기 로직을 실행하니까요.
- 악의적인 수신자에서 컨트랙트로 다시 호출해 보는 테스트요. 런칭 전에 이걸 찾아내는 방법이에요.
금융 조언이 아니라 교육이에요. 여기 나온 어떤 내용도 특정 프로토콜을 쓰라거나 피하라고 권하는 게 아니에요.