Skip to content

feat(week-03): complete week-03 assignment#34

Closed
sweatbuckets wants to merge 6 commits intoBay-17th:mainfrom
sweatbuckets:sweatbuckets/week-03
Closed

feat(week-03): complete week-03 assignment#34
sweatbuckets wants to merge 6 commits intoBay-17th:mainfrom
sweatbuckets:sweatbuckets/week-03

Conversation

@sweatbuckets
Copy link

@sweatbuckets sweatbuckets commented Feb 24, 2026

과제 제출 정보

주차: Week <03>

과제 유형:

  • 이론 (Theory Quiz)
  • 개발 (Dev Assignment)

구현 내용

  • CEI 패턴을 활용한 재진입 공격 방지
  • ReentrancyGuard를 활용한 재진입 공격 방지
  • payable(to).transfer(amount) 구문이 아닌 to.call{value: amount}()을 활용한 ETH 송금 로직

배운 점 (What I Learned)

이번 주에 배운 것 (2-3가지)

  1. 컨트랙트가 직접 ETH를 받았을 때 자동으로 호출되는 특수 함수 receive()를 마음대로 정의할 수 있다,
  2. receive()를 활용하면 한 트랜잭션 내부에서 외부 함수를 호출하는 구조를 설계할 수 있다.
  3. 컨트랙트의 외부 호출(ETH 전송)이 내부의 또 다른 호출을 유발할 수 있다는 점을 간과하고 Vault를 설계할 만큼 허술했던 초기 스마트컨트랙트의 사고로부터 현재의 표준이 만들어졌다.

어려웠던 점과 해결 방법

어려웠던 점:
외부 함수를 호출하는 서로 다른 여러 함수가 하나의 함수에 담길 경우, 이 트랜잭션의 실행만으로 무수히 많은 상태를 변경할 수 있을 텐데 트랜잭션 단위의 원자성을 보장하는 EVM에서 이를 막지 않아도 되는지, 이처럼 예측이 어렵고 복잡도가 높은 트랜잭션이 시스템의 위협이 되진 않는지 의문이 들었다.

해결 방법:
하나의 트랜잭션이 복잡한 상태 변경을 유발하더라도 그에 상응하는 가스 비용이 청구되므로 EVM에게는 트랜잭션 개수 자체는 연산 복잡도와 논리적으로 관계없음을 알게 됐다. 다만 각 블록마다 포함될 수 있는 총 가스량 한도가 약 30,000,000 gas(이더리움 메인넷)로 이론상 트랜잭션의 연산 제한은 존재한다. 이보다 적다면 아무리 복잡한 연산이라도 그에 준하는 가스비용을 내고 실행하는 것을 시스템에서 막지 않는다는 것을 이해했다.

질문 사항

Vault.t.sol의 Attacker에서 현재 receive()는 attack 트랜잭션에서 실행되지만 VaultSecure 내부 withdraw의 외부 콜(external call) 프레임에서 처리되는 함수입니다. 현재 구조로는 withdraw 재진입이 막히면 revert가 상위 call (withdraw() 실패->receive() 실패 ... 첫 번째 withdraw 실패)까지 타고 올라와서 attack() 트랜잭션 자체가 롤백되는 구조 같습니다.

따라서 VaultSecureTest의 몇몇 테스트 함수에서 예상한 “첫 출금까지 유효” 상태가 만들어지지 않습니다. deposit()과 첫번째 withdraw까지는 유효하게 처리되는걸 전제로 attack()을 호출하는 test_ReentrancyAttack_AttackerGetsOnlyOwnDeposit(), test_ReentrancyAttack_CannotDrainVault() 두 개 테스트 함수 자체가 revert되며 성공 처리될 수 없어 보입니다.

image

CEI, ReentrancyGuard 모두 재진입하는 함수 호출을 revert 시키므로 현재 테스트를 성공시키기 위해선 receive()가 콜스택의 실패를 트랜잭션 전체에 전파하지 않고 프레임 단위로 흡수하도록 try-catch 구조로 돼야 하는게 아닌지 궁금합니다.

receive() external payable {
    if (attackCount < 5) {
        attackCount++;
        try vault.withdraw(attackAmount) {
            // 성공하면 로그 등 처리
        } catch {
            // 실패하면 그냥 무시하고 종료
        }
    }
}
  • 커밋 특이 사항:
    로컬 수정이 꼬여서 week-02의 커밋이 같이 올라왔습니다..
    ReentrancyGuard 라이브러리 같이 푸시돼서 추적 제외했습니다..
    포크한 디렉토리 최신화 완료

체크리스트

테스트

  • forge build 성공
  • forge test 모든 테스트 통과

제출 규칙

  • 브랜치명이 {username}/week-{XX} 형식
  • .env 파일이 커밋에 포함되지 않음
  • 커밋 메시지가 규칙을 따름

@ahwlsqja
Copy link
Member

ahwlsqja commented Feb 25, 2026

리뷰

개발 과제

VaultSecure.sol - ReentrancyGuard + CEI 패턴 둘 다 적용한 거 아주 좋습니다!!
nonReentrant modifier + 상태 먼저 변경 후 call 순서도 정확합니다!!
call{value: amount}("") 사용한 것도 좋아요. transfer보다 가스 제한 없이 유연합니다.

Week-02 SimpleStorage도 deposit/withdraw 정확하고, NatSpec 주석에 본인 이해를 덧붙인 것 좋습니다.
msg.value는 함수 호출 시 전송된 ETH 양입니다 -> 트랜잭션의 value 필드를 msg 객체로 접근 가능 이런 식의 코멘트 진짜 좋아요.

퀴즈

Week-02 퀴즈 10/10 정확합니다.
특히 Q3 디지털 서명 세 속성(인증/무결성/부인 방지) 설명이 매우 정확하고 깊이가 있습니다 ㅎㅎ
Q5 nonce 재사용 공격에서 UTXO vs Account 모델 비교한 것도 훌륭합니다.

Week-03 퀴즈 10/10 정확합니다.
Q4 CEI 패턴 설명, Q6 재진입 공격 단계별 분석, Q8 tx.origin 취약점 시나리오 모두 정확합니다.
Q10 다이어그램 분석에서 공격자 탈취 금액 계산도 정확하고요.

몇 가지 참고:

  • Q6 수정 코드에서 require 검증이 빠져있는것 같습니다. CEI 패턴이라도 Checks는 필수입니다
  • Q9에서 function withdraw(...) public is nonReentrantis 빼야 합니다. public nonReentrant가 맞아요

배운 점

receive() 함수를 통한 재진입 공격 메커니즘 이해가 매우 깊습니다.
가스 한도와 트랜잭션 복잡도의 관계에 대한 의문도 좋은 질문이고, 본인이 잘 해결했네요.
블록 가스 한도 30M gas 맞습니다.

질문 답변

receive()에서 try-catch 구조로 돼야 하는게 아닌지

맞습니다.... 제가 테스트 작성에 실수를 한 것 같습니다... ㅠ

문제 원인: receive()에서 vault.withdraw()가 revert → receive() revert → call returns false → require(success) revert → 첫 번째 withdraw까지 전체 롤백

수정 내용: Attacker의 receive()에 try/catch 추가해서 revert 전파를 막았습니다.
main 브랜치에 이미 반영됐으니 pull 받으면 순수 CEI만으로도 테스트 통과됩니다.

@ahwlsqja ahwlsqja closed this Feb 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants