스마트 계약 보안 101 — 재진입 및 일반적인 AI 생성 실수

발행: (2026년 1월 12일 오후 03:32 GMT+9)
13 min read
원문: Dev.to

Source: Dev.to

Web3 여정을 시작한 지 30일 차에 “보안”이라는 단어가 무서운 감사 용어에서 벗어나 매우 실감나는 것이 되었다.

첫 번째로 “무해한” withdraw 함수가 전체 계약을 고갈시켰을 때는 해킹이라기보다 정상적인 결제처럼 보였다.

  1. 사용자가 Withdraw 버튼을 클릭했다.
  2. 계약이 그에게 ETH를 보냈다.
  3. 모든 것이 정상적으로 보였지만—예외는 결제가 실제로 끝나지 않았다는 점이다. 계속해서 다시 호출되고, 또 다시 호출되고, 또 다시 호출되었다.

계약이 무슨 일이 일어났는지 “깨달았을” 때쯤에는 이미 잔액이 사라져 있었다.

이것이 **재진입(reentrancy)**이며, AI가 Solidity 코드를 그 어느 때보다 빠르게 작성하도록 도와주면서 이 버그를 우연히 배포하는 것이 위험할 정도로 쉬워졌다.

재진입성(리엔트런시)이란 무엇인가

계약을 자판기와 같이 생각해 보세요:

  1. 동전을 넣습니다.
  2. 기계가 잔액을 확인합니다.
  3. 그런 다음 잔액을 업데이트하고 간식을 내립니다.

이제 버그를 상상해 보세요. 간식 문이 아직 열려 있는 동안 기계가 잔액을 업데이트하기 전에 “배출” 버튼을 반복해서 누를 수 있는 상황입니다. 기계는 당신이 한 개만 가져갔다고 생각하는데도 계속 간식을 잡아챕니다.

이것이 Web3에서 말하는 재진입성입니다:

  • 스마트 계약이 ETH 또는 토큰을 다른 주소/계약으로 보냅니다.
  • 전송이 진행되는 동안 수신자가 코드를 실행합니다.
  • 그 코드가 잔액이나 상태가 업데이트되기 원래 계약의 함수로 다시 호출합니다.
  • 공격자가 이 루프를 반복해 자금을 고갈시킵니다.

핵심 아이디어: 계약은 외부 호출이 “그냥 전송”이라고 신뢰합니다. 하지만 이더리움에서는 수신자가 자체 로직을 가진 계약일 수 있습니다.

클래식 재진입 함정 (AI가 왜 좋아하는가)

다음은 “간단한 출금 함수”를 요청했을 때 AI 도구가 자주 생성하는 취약한 패턴입니다:

// 1️⃣ Check if msg.sender has enough balance
require(balances[msg.sender] >= amount, "Insufficient balance");

// 2️⃣ Send ETH
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Transfer failed");

// 3️⃣ Update balance
balances[msg.sender] = 0;

이 코드는 영어로는 완전히 논리적으로 보이지만, 연산 순서가 치명적입니다:

  1. 외부 호출이 이루어짐 (공격자 계약으로).
  2. 공격자의 fallback 함수가 실행되어 withdraw()를 다시 호출함.
  3. 원래 계약은 아직 잔액을 0으로 설정하지 않았기 때문에 공격자가 여전히 잔액이 있다고 생각함.
  4. 계약이 비워질 때까지 자금이 계속 흐름.

AI 모델은 종종 다음과 같은 실수를 합니다:

  • 재진입 위험을 언급하지 않은 채 call{value: …}("")를 사용한다.
  • 재진입 방지 가드나 checks‑effects‑interactions 패턴을 사용하지 않는다.
  • “간단한”, “최소한의”, 혹은 “가스 효율적인” 코드를 요구할 때, “작동하는 예시”를 “안전한 예시”보다 우선시한다.

코드는 컴파일되고, 테스트도 통과합니다. 하지만 메인넷에서는 용서하지 않습니다.

Checks‑Effects‑Interactions: 첫 번째 방패

  • 대신에: “스낵을 보내고 → 잔액을 업데이트”
  • 다음과 같이: “잔액을 업데이트하고 → 스낵을 보낸다”

Solidity에서는 이를 checks‑effects‑interactions 라고 부릅니다:

단계수행 내용
Checks조건 검증 (require 문).
Effects내부 상태 업데이트 (잔액, 매핑).
Interactions외부 계약을 호출하거나 ETH를 마지막에 전송.

왜 작동하나요: 공격자가 재진입을 시도하면, 계약의 상태가 이미 “잔액이 없습니다”라고 표시됩니다. 따라서 다시 호출하더라도 require가 실패합니다.

call, delegatecall, 또는 transfer 다음에 상태 변화가 있는 경우, 여러분의 “재진입 레이더”가 울려야 합니다.

Source:

재진입 방지 가드 및 안전 패턴

현대 Solidity는 거의 항상 사용해야 하는 추가적인 안전망을 제공합니다:

  • ReentrancyGuard – 함수가 끝나기 전에 다시 진입하는 것을 차단하는 간단한 “잠금” 플래그.
  • Pull‑over‑push 결제 – 자동으로 ETH를 전송하는 대신, 사용자가 최소한의 제어된 함수에서 인출하도록 함.
  • 외부 호출 최소화 – 외부 호출이 적을수록 재진입 위험도 감소.

ReentrancyGuard를 자동판매기 문에 “사용 중” 표지판을 붙이는 것에 비유해 보세요: 한 번에 한 스낵만 배출되므로, 같은 사람이 백도어를 통해 버튼을 스팸하더라도 다른 사람이 버튼을 누를 수 없습니다.

초보자(특히 AI를 활용하는 경우)에게 권장되는 방법:

  • ETH를 전송하거나 신뢰할 수 없는 계약을 호출하는 모든 함수에 기본적으로 ReentrancyGuard를 사용하세요.
  • 해당 가드 없이도 안전하다는 것을 완전히 이해했을 때만 제거하십시오.

Source:

일반적인 AI‑생성 보안 실수 (재진입 외)

재진입은 하나의 카테고리에 불과합니다. AI는 몇 가지 위험한 패턴을 반복하는 경향이 있습니다:

실수위험한 이유
액세스 제어 누락 – 관리자 전용 함수(setPrice, pause, withdrawAll)를 public으로 남겨 둠.누구나 호출할 수 있습니다.
msg.sender에 대한 맹목적인 신뢰 – 역할 검사(onlyOwner, AccessControl) 없음.무단 행위자에 대한 보호가 없습니다.
배열에 대한 무제한 루프 – 가스가 부족해질 수 있는 대규모 사용자 리스트에 대한 “단순” 루프.그리핑이나 DoS에 완벽합니다.
반환값 무시 – 토큰 transfer 혹은 call이 실제로 성공했는지 확인하지 않음.자금이 묶이는 조용한 실패가 발생합니다.

이 모든 것은 처음 보면 합리적으로 보입니다. 바로 이 점이 위험한 이유입니다: 컴파일러에서는 문제가 없지만 실제 운영 환경에서 실패하기 때문입니다.

초보자의 목표는 “완벽한” 코드를 작성하는 것이 아니라, 문제가 발생할 수 있는 부분을 인식하고 그 지점에서 속도를 늦추는 것입니다.

개발자를 위한 중요성

  • 금고를 비웁니다.
  • 사용자 신뢰를 깨뜨립니다.
  • 당신의 이름을 체인 상에서 영원히 따라다닙니다.

이것에 대해 생각해 보세요 (미니 챌린지)

다음에 AI 도구에 물어볼 때:

“사용자가 ETH를 입금하고 출금할 수 있는 간단한 Solidity 계약을 작성해 주세요.”

다음과 같이 하세요:

  1. 외부 호출을 검사 (call, transfer, send).
  2. 순서를 확인: 함수가 ETH를 보내기 에 상태를 업데이트합니까, 에 업데이트합니까?
  3. 스스로에게 물어보세요: “수신자가 바로 여기서 콜백을 호출하는 계약이라면 어떻게 될까요?”

조금이라도 의심이 든다면, 좋습니다—그것이 보안 본능이 깨어난 것입니다.

Key Takeaway

Reentrancy는 마법이 아니다. 그것은 단지 “내 집을 잠그기 전에 외부 코드를 신뢰했다.” 와 같다.

AI가 우리 스마트‑컨트랙트 코드를 더 많이 작성하기 시작하면서, 항상 다음을 확인하라:

  • checks‑effects‑interactions 적용하기.
  • ReentrancyGuard (또는 동등한 것) 사용하기.
  • 모든 외부 호출을 신중히 검토하기.

안전하게, 호기심을 유지하고, 책임감 있게 구축하라.

Solidity, 당신의 강점은 더 빠르게 타이핑하는 것이 아니다.
AI가 남긴 함정문을 찾아내는 것이 강점이다.

방 안에서 가장 똑똑한 감사자가 되려 하지 마라.
명백한 버그를 절대 배포하지 않는 개발자가 되라.

Web3에서는 하나의 “무고한” withdraw 함수가 다음 사이의 차이를 만들 수 있다:

  • “멋진 작은 dApp을 배포했어.”
  • “드레인된 계약 기억해? 그래… 그거 내 거였어.”

더 깊이 탐구할 자료

Back to Blog

관련 글

더 보기 »

내 “완벽한” 계약을 깨뜨린 테스트

Day 31 – 왜 Dev Tools가 그 어느 때보다 중요한가 첫 번째로 테스트가 내 “완벽한” 스마트 계약을 파괴했을 때, 해커가 아니었다. 그것은 내 자신의 Dev 환경이었다. 나는…

Web3 스토어

개요: 학습 연습으로 Solidity와 ethers.js를 사용하여 데모 Web3 스토어를 작성하고 배포했습니다. 오래된 책에서 권장된 원래 도구인 web3.js, T...