스마트 계약 보안 101 — 재진입 및 일반적인 AI 생성 실수
Source: Dev.to
Web3 여정을 시작한 지 30일 차에 “보안”이라는 단어가 무서운 감사 용어에서 벗어나 매우 실감나는 것이 되었다.
첫 번째로 “무해한” withdraw 함수가 전체 계약을 고갈시켰을 때는 해킹이라기보다 정상적인 결제처럼 보였다.
- 사용자가 Withdraw 버튼을 클릭했다.
- 계약이 그에게 ETH를 보냈다.
- 모든 것이 정상적으로 보였지만—예외는 결제가 실제로 끝나지 않았다는 점이다. 계속해서 다시 호출되고, 또 다시 호출되고, 또 다시 호출되었다.
계약이 무슨 일이 일어났는지 “깨달았을” 때쯤에는 이미 잔액이 사라져 있었다.
이것이 **재진입(reentrancy)**이며, AI가 Solidity 코드를 그 어느 때보다 빠르게 작성하도록 도와주면서 이 버그를 우연히 배포하는 것이 위험할 정도로 쉬워졌다.
재진입성(리엔트런시)이란 무엇인가
계약을 자판기와 같이 생각해 보세요:
- 동전을 넣습니다.
- 기계가 잔액을 확인합니다.
- 그런 다음 잔액을 업데이트하고 간식을 내립니다.
이제 버그를 상상해 보세요. 간식 문이 아직 열려 있는 동안 기계가 잔액을 업데이트하기 전에 “배출” 버튼을 반복해서 누를 수 있는 상황입니다. 기계는 당신이 한 개만 가져갔다고 생각하는데도 계속 간식을 잡아챕니다.
이것이 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;
이 코드는 영어로는 완전히 논리적으로 보이지만, 연산 순서가 치명적입니다:
- 외부 호출이 이루어짐 (공격자 계약으로).
- 공격자의 fallback 함수가 실행되어
withdraw()를 다시 호출함. - 원래 계약은 아직 잔액을 0으로 설정하지 않았기 때문에 공격자가 여전히 잔액이 있다고 생각함.
- 계약이 비워질 때까지 자금이 계속 흐름.
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 계약을 작성해 주세요.”
다음과 같이 하세요:
- 외부 호출을 검사 (
call,transfer,send). - 순서를 확인: 함수가 ETH를 보내기 전에 상태를 업데이트합니까, 후에 업데이트합니까?
- 스스로에게 물어보세요: “수신자가 바로 여기서 콜백을 호출하는 계약이라면 어떻게 될까요?”
조금이라도 의심이 든다면, 좋습니다—그것이 보안 본능이 깨어난 것입니다.
Key Takeaway
Reentrancy는 마법이 아니다. 그것은 단지 “내 집을 잠그기 전에 외부 코드를 신뢰했다.” 와 같다.
AI가 우리 스마트‑컨트랙트 코드를 더 많이 작성하기 시작하면서, 항상 다음을 확인하라:
- checks‑effects‑interactions 적용하기.
ReentrancyGuard(또는 동등한 것) 사용하기.- 모든 외부 호출을 신중히 검토하기.
안전하게, 호기심을 유지하고, 책임감 있게 구축하라.
Solidity, 당신의 강점은 더 빠르게 타이핑하는 것이 아니다.
AI가 남긴 함정문을 찾아내는 것이 강점이다.
방 안에서 가장 똑똑한 감사자가 되려 하지 마라.
명백한 버그를 절대 배포하지 않는 개발자가 되라.
Web3에서는 하나의 “무고한” withdraw 함수가 다음 사이의 차이를 만들 수 있다:
- “멋진 작은 dApp을 배포했어.”
- “드레인된 계약 기억해? 그래… 그거 내 거였어.”
더 깊이 탐구할 자료
-
Solidity Docs — Security Considerations – Reentrancy section
외부 호출이 위험한 이유와 상태 변화를 안전하게 구조화하는 방법을 설명하는 공식 언어 문서. -
Consensys Diligence — Reentrancy (Smart Contract Best Practices) – Link
공격 패턴, 체크‑이펙트‑인터랙션, 그리고 일반적인 함정에 대한 고전적인 참고 자료. -
OpenZeppelin Contracts — ReentrancyGuard – Link
사실상의 표준 구현인 재진입 방지 락; 설명한 패턴을 실제로 사용하고자 하는 독자에게 완벽한 후속 자료. -
Other resources:
-
Follow the series:
-
Telegram에서 **Web3ForHumans**에 참여하고 함께 Web3을 브레인스토밍해요.