무료 미리 보기10분 분량

재진입: 아직도 통하는 가장 오래된 수법

자기 기록을 갱신하기 전에 돈을 먼저 보내는 컨트랙트는, 보내는 중간에 그 돈을 다시 요구받을 수 있어요. 그게 재진입(reentrancy)이고, Ethereum 역사상 첫 번째 대형 참사의 원인이었고, 지금도 피해자를 찾아내고 있어요.

작동 방식

  1. 컨트랙트가 예치자들의 잔고를 보관하고 출금 함수를 제공해요.
  2. 출금 함수는 자금을 먼저 보내고, 그다음에 예치자의 잔고를 0으로 만들어요.
  3. 다른 컨트랙트로 자금을 보내면 그 컨트랙트의 코드로 제어권이 넘어가요.
  4. 그 코드가 출금을 다시 호출해요. 잔고가 아직 0이 되지 않았으니 확인을 두 번째로도 통과해요.
  5. 풀이 빌 때까지 이 반복이 계속돼요.

해법은 확인, 변경, 상호작용이라고 불리는 순서 규칙이에요. 먼저 검증하고, 두 번째로 내 상태를 갱신하고, 마지막에 다른 쪽과 이야기하세요. 첫 호출이 열려 있는 동안 두 번째 진입을 거부하는 잠금 장치인 재진입 가드는 그 위에 더하는 안전장치예요.

어디에서 일어났을까요

  • The DAO, 2016년 6월. 재진입 결함으로 Ethereum 위에 만들어진 투자 펀드에서 약 360만 ETH가 빠져나갔어요. 그 대응이 체인을 갈라놨어요. 다수는 도난을 되돌리려고 포크했고, 되돌리기를 거부한 체인은 오늘날 Ethereum Classic으로 이어지고 있어요.
  • Curve Finance, 2023년 7월. 결함은 Curve 자체 로직이 아니라 Vyper 컴파일러의 특정 버전들에 있었고, 거기서는 재진입 가드가 적힌 대로 작동하지 않았어요. 여러 풀이 비워졌고 손실은 약 7000만 달러로 보고됐어요. 감사받은 코드도 그걸 컴파일하는 도구 때문에 무너질 수 있어요.

왜 살아남았을까요

  • 어떤 외부 호출이든 제어권을 넘겨줄 수 있어요. 토큰 전송 훅, 콜백, 그리고 받는 쪽에 알려 주는 최신 토큰 표준들까지요.
  • 읽기 전용 재진입은 더 미묘해요. 공격자가 상태가 절반만 갱신된 동안 조회 함수로 재진입하면, 다른 프로토콜이 잘못된 가격이나 잔고를 읽고 그걸로 움직여요.
  • 함수 간 재진입은 같은 상태를 공유하는 다른 함수를 쓰기 때문에, 한 함수에만 가드를 걸어 두는 건 충분하지 않아요.

실제로 막아 주는 것

  • 확인, 변경, 상호작용을 뻔한 출금 함수뿐 아니라 어디에나 적용하기요.
  • 외부 호출을 하는 모든 상태 변경 진입점에 재진입 가드를 걸기요.
  • 모든 토큰을 적대적인 코드로 취급하기요. 토큰 컨트랙트는 전송 중에 자기 로직을 실행하니까요.
  • 악의적인 수신자에서 컨트랙트로 다시 호출해 보는 테스트요. 런칭 전에 이걸 찾아내는 방법이에요.
금융 조언이 아니라 교육이에요. 여기 나온 어떤 내용도 특정 프로토콜을 쓰라거나 피하라고 권하는 게 아니에요.