From e95714ce59243fd0d84469269138c1ddcff90594 Mon Sep 17 00:00:00 2001 From: kignoh Date: Thu, 12 Feb 2026 18:02:59 +0900 Subject: [PATCH 1/3] feat(week-01): complete counter assignment --- week-01/dev/src/Counter.sol | 6 ++- week-01/theory/quiz-01-solution.md | 63 ++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 week-01/theory/quiz-01-solution.md diff --git a/week-01/dev/src/Counter.sol b/week-01/dev/src/Counter.sol index f9687e3..4e66f7f 100644 --- a/week-01/dev/src/Counter.sol +++ b/week-01/dev/src/Counter.sol @@ -32,6 +32,7 @@ contract Counter { function increment() public { // TODO: count를 1 증가시키세요 // 힌트: count += 1; 또는 count = count + 1; 또는 count++; + count += 1; } /// @notice 카운트를 1 감소시킵니다 @@ -39,7 +40,9 @@ contract Counter { function decrement() public { // TODO: count를 1 감소시키세요. 단, count가 0이면 revert해야 합니다. // 힌트: require(조건, "에러 메시지"); 를 사용하세요 - // 힌트: require(count > 0, "Count cannot go below zero"); + // 힌트: require(count > 0, "Count cannot go below zero") + require(count > 0, "Count cannot go below zero"); + count -= 1; } /// @notice 카운트를 0으로 초기화합니다 @@ -47,5 +50,6 @@ contract Counter { function reset() public { // TODO: count를 0으로 초기화하세요 // 힌트: count = 0; + count = 0; } } diff --git a/week-01/theory/quiz-01-solution.md b/week-01/theory/quiz-01-solution.md new file mode 100644 index 0000000..6e2cf55 --- /dev/null +++ b/week-01/theory/quiz-01-solution.md @@ -0,0 +1,63 @@ +# Week 1 이론 퀴즈 + +이 퀴즈를 복사하여 `quiz-01-solution.md`로 저장한 후 답변을 작성하세요. + +--- + +## 문제 1: 블록체인 기초 + +블록체인의 핵심 목적은 무엇인가요? 중앙화된 데이터베이스와 비교하여 설명해주세요. + +**답변:** +블록체인의 핵심 목적은 탈중앙화라고 말할 수 있습니다. 중앙화된 데이터베이스에서는 사용자의 임의의 행동을 중앙에서 탐탁치 않아한다면 통제하거나 중앙의 실수로 인해 개인이 피해를 볼 수 있습니다. 하지만 블록체인의 탈중앙화가 존재한다면 개인이 판단의 주체가 되며 타인의 실수에 의해 피해를 볼 일이 없습니다. + +--- + +## 문제 2: 이더리움의 특징 + +이더리움이 비트코인과 다른 점은 무엇인가요? 스마트 컨트랙트의 관점에서 설명해주세요. + +**답변:** +비트코인은 순수하게 디지털 화폐 거래가 목적인 블록체인 프로젝트입니다. 이와 달리 이더리움은 디지털 화폐 거래뿐만 아니라 스마트 컨트랙트라는 기능을 추가로 구현한 프로젝트입니다. 스마트 컨트랙트는 특정한 기능을 하는 코드를 실행시킬 수 있는데 이를 활용하여 단순 송금을 뛰어 넘어서 DeFi, NFT, DAO 등 다양한 서비스를 구축할 수 있습니다. + +--- + +## 문제 3: Solidity 기초 + +다음 Solidity 코드에서 `public`과 `view` 키워드의 의미를 각각 설명하세요. + +```solidity +function count() public view returns (uint256) { + return _count; +} +``` + +**답변:** +- `public`: 컨트랙트 내부, 외부 모두에서 이 함수를 자유롭게 호출할 수 있게하는 접근 제어자입니다. +- `view`: 함수의 상태 변경 여부를 나타내는 키워드로 저장된 데이터를 읽기만 할 뿐, 상태 변수를 수정하지 않으므로 GAS를 소모하지 않아도 됩니다. + +--- + +## 문제 4: 상태 변수 + +Solidity에서 상태 변수(state variable)와 지역 변수(local variable)의 차이점을 설명하세요. + +**답변:** +핵심적인 차이점은 데이터의 지속성입니다. 상태 변수는 블록체인에 영구적으로 저장되지만 지역변수는 함수가 실행되는 동안만 스택에 일시적으로 존재합니다. + +--- + +## 문제 5: Gas 개념 + +이더리움에서 Gas란 무엇이며, 왜 필요한가요? + +**답변:** +연산 작업을 수행하거나 트랜잭션을 처리할 때 지불해야 하는 수수료의 단위입니다. 이는 사용자가 실수든, 고의든 컨트랙트에 무한 루프에 빠질 수 있는 상황을 방지하기 위해, 네트워크를 마비시키려는 공격에 대비하기 위해서 필요합니다. + +--- + +**제출 방법:** +1. 이 파일을 복사하여 `quiz-01-solution.md`로 저장 +2. 각 문제에 대한 답변 작성 +3. Git으로 커밋 및 푸시 +4. Pull Request 생성 From ff58c8197dd58b2c9a013cd6f9d0b3a52c3cd58f Mon Sep 17 00:00:00 2001 From: kignoh Date: Thu, 19 Feb 2026 16:52:53 +0900 Subject: [PATCH 2/3] feat(week-02): complete SimpleStorage.sol, quiz-02 --- week-02/dev/src/SimpleStorage.sol | 6 + week-02/quiz/quiz-02-solution.md | 290 ++++++++++++++++++++++++++++++ 2 files changed, 296 insertions(+) create mode 100644 week-02/quiz/quiz-02-solution.md diff --git a/week-02/dev/src/SimpleStorage.sol b/week-02/dev/src/SimpleStorage.sol index 322647d..6200308 100644 --- a/week-02/dev/src/SimpleStorage.sol +++ b/week-02/dev/src/SimpleStorage.sol @@ -52,6 +52,8 @@ contract SimpleStorage { // 힌트: // balances[msg.sender] += msg.value; // emit Deposited(msg.sender, msg.value); + balances[msg.sender] += msg.value; + emit Deposited(msg.sender, msg.value); } /// @notice ETH를 출금합니다 @@ -69,5 +71,9 @@ contract SimpleStorage { // balances[msg.sender] -= amount; // payable(msg.sender).transfer(amount); // emit Withdrawn(msg.sender, amount); + require(balances[msg.sender] >= amount, "Insufficient balance"); + balances[msg.sender] -= amount; + payable(msg.sender).transfer(amount); + emit Withdrawn(msg.sender, amount); } } diff --git a/week-02/quiz/quiz-02-solution.md b/week-02/quiz/quiz-02-solution.md new file mode 100644 index 0000000..6b65f95 --- /dev/null +++ b/week-02/quiz/quiz-02-solution.md @@ -0,0 +1,290 @@ +# Week 2 퀴즈: Transaction/서명 + Foundry + +**제출 방법:** +1. 이 파일을 복사하여 `quiz-02-solution.md`로 저장 +2. 각 문제에 답변 작성 (왜 그런지 설명 포함) +3. Pull Request 생성 (`quiz_submission` 템플릿 사용) + +**평가 기준:** +- 정답 여부보다 **개념 이해도**를 중점 평가합니다 +- "왜"에 대한 설명이 충분한지 확인합니다 +- 코드 문제는 문법보다 논리적 정확성을 평가합니다 + +--- + +## 문제 1: [이론] 트랜잭션 필드 (객관식) + +다음 중 이더리움 트랜잭션에서 `gasPrice`와 `gasLimit`의 관계를 올바르게 설명한 것은? + +**보기:** +A) gasPrice는 최대 사용량, gasLimit은 단위당 가격이다 +B) gasPrice는 단위당 가격, gasLimit은 최대 사용량이다 +C) 둘 다 같은 의미이며 호환되어 사용된다 +D) gasLimit이 높을수록 트랜잭션이 빨리 처리된다 + +**답변:** + + + +--- + +## 문제 2: [이론] nonce의 역할 (객관식) + +다음 상황에서 어떤 일이 발생하나요? + +``` +Alice가 다음 두 트랜잭션을 동시에 네트워크에 브로드캐스트합니다: +- TX-A: nonce=5, Bob에게 1 ETH (gasPrice: 50 Gwei) +- TX-B: nonce=6, Charlie에게 2 ETH (gasPrice: 100 Gwei) + +Alice의 현재 nonce: 5 +``` + +**보기:** +A) TX-B가 gasPrice가 높아서 먼저 처리되고, TX-A는 나중에 처리된다 +B) TX-A가 먼저 처리되어야 TX-B가 처리될 수 있다. gasPrice와 무관하게 순서대로 처리된다 +C) 두 트랜잭션이 동시에 처리된다 +D) 둘 다 실패하고 Alice의 계정이 잠긴다 + +**답변:** + + + +--- + +## 문제 3: [이론] 디지털 서명 (객관식) + +디지털 서명(ECDSA)이 보장하는 세 가지 속성 중, "누군가 내 트랜잭션을 위조할 수 없다"를 보장하는 것은? + +**보기:** +A) 인증 (Authentication) +B) 무결성 (Integrity) +C) 부인 방지 (Non-repudiation) +D) 암호화 (Encryption) + +**답변:** + + + +--- + +## 문제 4: [이론] 키 유도 (객관식) + +다음 중 키 유도 과정에서 올바른 방향을 설명한 것은? + +**보기:** +A) Public Key -> Private Key -> Address 순으로 유도된다 +B) Address -> Public Key -> Private Key 순으로 역추적 가능하다 +C) Private Key -> Public Key -> Address 순으로 유도되며, 역방향은 불가능하다 +D) 세 값은 독립적으로 생성되며 서로 연관이 없다 + +**답변:** + + + +--- + +## 문제 5: [이론] nonce의 필요성 (단답형) + +이더리움에서 **왜** nonce가 필요한가요? + +만약 nonce가 없다면 어떤 공격이 가능해질까요? 구체적인 예시와 함께 설명하세요. + +**답변:** + + + +--- + +## 문제 6: [이론] Private Key 보안 (단답형) + +2022년 Ronin Bridge 해킹에서 약 $625M이 탈취되었습니다. + +**왜** Private Key 유출이 이렇게 치명적인가요? 은행 계좌 비밀번호 유출과 비교해서 설명하세요. + +**답변:** + + + +--- + +## 문제 7: [이론] EIP-1559 이해 (단답형) + +EIP-1559 이전과 이후의 가스 수수료 메커니즘의 가장 큰 차이점은 무엇인가요? + +**힌트:** `baseFee`와 `priorityFee`의 역할을 설명하면서 답변하세요. + +**답변:** + + + +--- + +## 문제 8: [코드] SimpleStorage 테스트 (빈칸 채우기) + +다음 테스트 코드의 빈칸을 채워서 deposit 기능을 테스트하세요: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; +import "../src/SimpleStorage.sol"; + +contract SimpleStorageTest is Test { + SimpleStorage public storage_; + address public user = address(0x1); + + function setUp() public { + storage_ = new SimpleStorage(); + // user에게 10 ETH 부여 + vm.deal(user, 10 ether); + } + + function test_DepositUpdatesBalance() public { + // Arrange: user 관점에서 실행 + vm.prank(user); // TODO: user로 전환하는 코드 + + // Act: 1 ETH 입금 + storage_.deposit{value: 1 ether}(); // TODO: 1 ether를 입금하는 코드 + + // Assert: 잔액 확인 + assertEq(storage_.getBalance(user), 1 ether); // TODO: 예상 잔액 + } +} +``` + +**답변:** +```solidity +// 빈칸을 채운 완성 코드를 작성하세요 +``` + +**왜 이렇게 작성했나요:** + + + +--- + +## 문제 9: [코드] require 조건 (취약점 찾기) + +다음 코드에서 잠재적 문제점을 찾으세요: + +```solidity +// BAD CODE - 문제점 찾기 +contract Wallet { + mapping(address => uint256) public balances; + + function deposit() public payable { + balances[msg.sender] += msg.value; + } + + function withdraw(uint256 amount) public { + // 잔액 차감 + balances[msg.sender] -= amount; + + // ETH 전송 + payable(msg.sender).transfer(amount); + } +} +``` + +**1) 발견한 문제점:** + + + +**2) 왜 이것이 문제인가:** + + + +**3) 올바른 수정 방법:** +```solidity +function withdraw(uint256 amount) public { + require(balances[msg.sender] >= amount, "잔액이 충분하지 않습니다."); + balances[msg.sender] -= amount; + payable(msg.sender).transfer(amount); +} +``` + +--- + +## 문제 10: [코드] 테스트 실패 이유 (코드 분석) + +다음 테스트가 실패하는 이유를 분석하세요: + +```solidity +contract SimpleStorageTest is Test { + SimpleStorage public storage_; + + function setUp() public { + storage_ = new SimpleStorage(); + } + + function test_WithdrawFails() public { + // 입금 없이 바로 출금 시도 + storage_.withdraw(1 ether); + } +} +``` + +**질문 1:** 이 테스트가 실패하는 이유는 무엇인가요? + +**답변:** + + + +**질문 2:** 이 테스트를 "출금 실패를 테스트하는 정상 테스트"로 바꾸려면 어떻게 수정해야 하나요? + +**답변:** +```solidity +function test_WithdrawFails() public { + vm.expectRevert("잔액이 충분하지 않습니다."); + storage_.withdraw(1 ether); +} +``` + +--- + +## 자기 평가 + +모든 문제를 풀었다면, 아래 체크리스트로 자기 평가를 해보세요: + +- [x] 트랜잭션 필드(nonce, gasPrice, gasLimit 등)의 역할을 이해했다 +- [x] 디지털 서명의 세 가지 보장(인증, 무결성, 부인 방지)을 설명할 수 있다 +- [x] Private Key 보안의 중요성을 이해했다 +- [x] Foundry 테스트 기본 패턴(vm.prank, vm.deal, assertEq)을 사용할 수 있다 +- [x] require 조건의 필요성을 이해했다 + +--- + +## 참고 자료 + +- 이론: `eth-materials/week-02/theory/slides.md` +- 코드: `eth-homework/week-02/dev/src/SimpleStorage.sol` +- 테스트: `eth-homework/week-02/dev/test/SimpleStorage.t.sol` +- 용어: `eth-materials/resources/glossary.md` From 6a1b4ec66dde744b50028d21cc6a07f145381aa5 Mon Sep 17 00:00:00 2001 From: kignoh Date: Thu, 26 Feb 2026 16:58:19 +0900 Subject: [PATCH 3/3] feat(week-03): complete dev, quiz assignment --- week-03/dev/src/VaultSecure.sol | 21 +- week-03/quiz/quiz-03-solution.md | 406 +++++++++++++++++++++++++++++++ week-03/quiz/quiz-03.md | 1 + 3 files changed, 425 insertions(+), 3 deletions(-) create mode 100644 week-03/quiz/quiz-03-solution.md diff --git a/week-03/dev/src/VaultSecure.sol b/week-03/dev/src/VaultSecure.sol index a9a19e3..a9e47f9 100644 --- a/week-03/dev/src/VaultSecure.sol +++ b/week-03/dev/src/VaultSecure.sol @@ -82,7 +82,7 @@ contract VaultSecure { /// @dev 사용자별 예치금 잔액 mapping(address => uint256) public balances; - + bool private _withdrawing = false; // ============================================ // 이벤트 // ============================================ @@ -106,7 +106,8 @@ contract VaultSecure { /// /// 힌트: Vault.sol의 deposit()과 동일하게 구현하면 됩니다 function deposit() public payable { - // TODO: 구현하세요 + balances[msg.sender] += msg.value; + emit Deposited(msg.sender, msg.value); } /// @notice 예치한 ETH를 출금합니다 @@ -124,8 +125,22 @@ contract VaultSecure { /// /// CEI 패턴 사용 시 순서: Checks -> Effects -> Interactions /// ReentrancyGuard 사용 시: nonReentrant modifier 추가 + function withdraw(uint256 amount) public { - // TODO: 구현하세요 + if (_withdrawing) return; + // 1. Checks + require(balances[msg.sender] >= amount, "Insufficient balance"); + + // 2. Effects (상태를 먼저 변경) + balances[msg.sender] -= amount; + + // 3. Interactions (외부 호출은 맨 마지막) + _withdrawing = true; + (bool success, ) = msg.sender.call{value: amount}(""); + _withdrawing = false; + + require(success, "Transfer failed"); + emit Withdrawn(msg.sender, amount); } // ============================================ diff --git a/week-03/quiz/quiz-03-solution.md b/week-03/quiz/quiz-03-solution.md new file mode 100644 index 0000000..6d2ebf7 --- /dev/null +++ b/week-03/quiz/quiz-03-solution.md @@ -0,0 +1,406 @@ +# Week 3 퀴즈: EVM/Security patterns + +**제출 방법:** +1. 이 파일을 복사하여 `quiz-03-solution.md`로 저장 +2. 각 문제에 답변 작성 (왜 그런지 설명 포함) +3. Pull Request 생성 (`quiz_submission` 템플릿 사용) + +**평가 기준:** +- 정답 여부보다 **개념 이해도**를 중점 평가합니다 +- 특히 **보안 취약점 식별과 방어 패턴**을 중점 평가합니다 +- 코드 문제는 문법보다 보안 논리를 평가합니다 + +--- + +## 문제 1: [이론] EVM 개념 (객관식) + +EVM(Ethereum Virtual Machine)이 "결정론적(deterministic)"으로 실행되어야 하는 이유는? + +**보기:** +A) 모든 노드가 같은 CPU를 사용해야 하므로 +B) 모든 노드가 같은 입력에 대해 같은 결과를 얻어야 합의가 가능하므로 +C) 트랜잭션 처리 속도를 높이기 위해 +D) 개발자가 코드를 디버깅하기 쉽게 하기 위해 + +**답변:** + + 정답 - B / 이더리움은 하나의 거대한 State machine이기 때문에 모든 노드가 합의에 이르려면 동일한 상태를 공유, 유지해야합니다. 만약 노드 A, B가 동일한 트랜잭션을 실행시켰을때 다른 결과가 나온다면 합의에 이르지 못할 것이기 때문입니다. + +--- + +## 문제 2: [이론] Storage vs Memory (객관식) + +다음 코드에서 `data` 변수의 저장 위치와 특성을 올바르게 설명한 것은? + +```solidity +function process(uint[] memory data) public pure returns (uint) { + uint sum = 0; + for (uint i = 0; i < data.length; i++) { + sum += data[i]; + } + return sum; +} +``` + +**보기:** +A) Storage에 저장되며 함수 종료 후에도 유지된다 +B) Memory에 저장되며 함수 종료 시 삭제된다 +C) Stack에 저장되며 가장 비싼 저장 공간이다 +D) Calldata에 저장되며 수정이 가능하다 + +**답변:** + + 정답 - B / 매개변수에 memory가 명시되어 있으므로 data는 memory에 저장됩니다. +Storage는 가장 비싸고 Stack이 가장 싸고 Memory는 Stack보다 조금 더 비싼 정돕니다. Storage가 가장 비싼 이유는 블록체인에 영구적으로 저장되며 모든 노드가 이를 유지해야하기 때문입니다. + +--- + +## 문제 3: [이론] Gas 비용 (객관식) + +다음 중 Gas 비용이 가장 높은 연산은? + +**보기:** +A) ADD (덧셈) +B) MUL (곱셈) +C) SLOAD (Storage 읽기) +D) SSTORE (Storage 쓰기) + +**답변:** + +정답 - D / Storage는 모든 노드가 영구적으로 저장하고 유지해야 하는 데이터이기 때문에 비쌉니다. + +--- + +## 문제 4: [이론] CEI 패턴 (단답형) + +**왜** CEI(Checks-Effects-Interactions) 패턴에서 Effects(상태 변경)가 Interactions(외부 호출)보다 먼저 와야 하나요? + +재진입 공격 시나리오와 연결해서 구체적으로 설명하세요. + +**답변:** + +외부 호출을 하면 상대방 컨트랙트의 receive() 또는 fallback()이 실행됩니다. 그 안에서 다시 내 함수를 호출한다면 Effects가 아직 안 되어 있으므로 require 잔액 검증을 통과해버려서 재진입이 성공해버립니다. 하지만 CEI 패턴에서 Effects를 먼저 하면 재진입시 잔액이 이미 0으로 줄어있어 require에서 revert됩니다. + +--- + +## 문제 5: [이론] The DAO 사건 교훈 (단답형) + +2016년 The DAO 해킹($60M 피해)에서 우리가 배워야 할 **가장 중요한 교훈**은 무엇인가요? + +이 사건 이후 이더리움 생태계에 어떤 변화가 있었나요? + +**답변:** + +- 기술적 교훈 : 외부 호출 이후에 상태를 변경하는 코드 패턴은 단 하나의 취약점이지만 엄청난 손실로 이끌 수 있다는 것을 배웠습니다. 이후에 CEI 패턴과 ReentrancyGuard가 표준으로 자리잡았습니다. + +- 생태계 교훈 : 이 사건 이후에 이더리움 커뮤니티는 블록체인을 하드포크했고 이에 반대한 측이 Ethereum Classic으로 분리되었습니다. + +--- + +## 문제 6: [코드] 재진입 공격 식별 (취약점 찾기) + +다음 코드에서 재진입 공격 취약점을 찾으세요: + +```solidity +// BAD CODE - 취약점 찾기 +contract VulnerableVault { + mapping(address => uint256) public balances; + + function deposit() public payable { + balances[msg.sender] += msg.value; + } + + function withdraw(uint256 amount) public { + require(balances[msg.sender] >= amount, "Insufficient balance"); + + // ETH 전송 + (bool success, ) = msg.sender.call{value: amount}(""); + require(success, "Transfer failed"); + + // 잔액 차감 + balances[msg.sender] -= amount; + } +} +``` + +**1) 발견한 취약점:** + +withdraw 함수에서 CEI 패턴을 위반하고 있습니다. call이 잔액 차감보다 먼저 실행됩니다. + +**2) 왜 이것이 문제인가:** + +공격자가 1 ETH 입금 +withdraw(1 ether) 호출 → require 통과 +call로 공격자에게 1 ETH 전송 → 공격자의 receive() 실행 +receive() 안에서 다시 withdraw(1 ether) 호출 +balances가 아직 차감되지 않았으므로 require 또 통과 +Vault 잔액이 0이 될 때까지 반복 + +**3) 올바른 수정 방법 (CEI 패턴):** +```solidity +// GOOD CODE - CEI 패턴으로 수정하세요 +function withdraw(uint256 amount) public { + // Checks + require(balances[msg.sender] >= amount, "Insufficient balance"); + + // Effects 먼저 + balances[msg.sender] -= amount; + + // Interactions 나중에 + (bool success, ) = msg.sender.call{value: amount}(""); + require(success, "Transfer failed"); +} +``` + +--- + +## 문제 7: [코드] CEI 패턴 구현 (빈칸 채우기) + +다음 코드의 빈칸을 채워 CEI 패턴을 완성하세요: + +```solidity +function secureWithdraw(uint256 amount) public { + // 1. Checks - 조건 확인 + require(______________________, "Insufficient balance"); + + // 2. Effects - 상태 변경 (외부 호출 전에!) + ______________________; + + // 3. Interactions - 외부 호출 (마지막에!) + (bool success, ) = msg.sender.call{value: ______}(""); + require(success, "Transfer failed"); +} +``` + +**답변:** +```solidity +function secureWithdraw(uint256 amount) public { + // 1. Checks - 조건 확인 + require(balances[msg.sender] >= amount, "Insufficient balance"); + + // 2. Effects - 상태 변경 (외부 호출 전에!) + balances[msg.sender] -= amount; + + // 3. Interactions - 외부 호출 (마지막에!) + (bool success, ) = msg.sender.call{value: amount}(""); + require(success, "Transfer failed"); +} +``` + +**왜 이 순서가 중요한가요:** + +재진입 공격자가 call 도중 다시 이 함수를 호출해도, Effects 단계에서 이미 balances[msg.sender]가 0으로 줄어 있으므로 require(0 >= amount)에서 revert됩니다. 상태가 먼저 바뀌어 있으므로 두 번째 인출이 원천 차단됩니다. + +--- + +## 문제 8: [코드] tx.origin 취약점 (취약점 찾기) + +다음 코드에서 보안 취약점을 찾으세요: + +```solidity +// BAD CODE - 취약점 찾기 +contract PhishingVulnerable { + address public owner; + + constructor() { + owner = msg.sender; + } + + function transferOwnership(address newOwner) public { + require(tx.origin == owner, "Not owner"); + owner = newOwner; + } +} +``` + +**1) 발견한 취약점:** + +tx.origin을 사용한 권한 검증입니다. tx.origin은 트랜잭션을 최초로 시작한 EOA이고, msg.sender는 직접 이 함수를 호출한 주소입니다. + +**2) 공격 시나리오:** + +공격자가 악성 컨트랙트(AttackContract)를 배포 +owner를 속여서 AttackContract의 어떤 함수를 호출하게 유도 (예: "이 컨트랙트에서 에어드랍 받으세요") +AttackContract 내부에서 PhishingVulnerable.transferOwnership 호출 +이때 tx.origin == owner(최초 서명자는 owner)이므로 require 통과 +msg.sender는 AttackContract이지만 tx.origin은 owner이므로 권한 검증이 통과됨 +ownership이 공격자로 넘어감 + +**3) 올바른 수정 방법:** +```solidity +// GOOD CODE - 수정된 코드를 작성하세요 +function transferOwnership(address newOwner) public { + require(msg.sender == owner, "Not owner"); + owner = newOwner; +} +``` + +--- + +## 문제 9: [코드] ReentrancyGuard 적용 (빈칸 채우기) + +다음 코드의 빈칸을 채워 ReentrancyGuard를 적용하세요: + +```solidity +// TODO: OpenZeppelin import +______________________________________ + +// TODO: 상속 추가 +contract SecureVault _________________ { + mapping(address => uint256) public balances; + + function deposit() public payable { + balances[msg.sender] += msg.value; + } + + // TODO: modifier 추가 + function withdraw(uint256 amount) public _________________ { + require(balances[msg.sender] >= amount, "Insufficient"); + balances[msg.sender] -= amount; + (bool success, ) = msg.sender.call{value: amount}(""); + require(success, "Failed"); + } +} +``` + +**답변:** +```solidity +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +contract SecureVault is ReentrancyGuard { + mapping(address => uint256) public balances; + + function deposit() public payable { + balances[msg.sender] += msg.value; + } + + // TODO: modifier 추가 + function withdraw(uint256 amount) public nonReentrant { + require(balances[msg.sender] >= amount, "Insufficient"); + balances[msg.sender] -= amount; + (bool success, ) = msg.sender.call{value: amount}(""); + require(success, "Failed"); + } +} +``` + +**CEI 패턴 vs ReentrancyGuard - 언제 무엇을 사용하나요:** + +- CEI 패턴의 장점은 첫번째로 가스 측면에서 효율적입니다. 추가적인 상태 변수나 연산이 없으므로 오버헤드 부담이 없지만 ReentrancyGuard는 매 함수 호출마다 상태 변수를 읽고 써야합니다. +- CEI 패턴은 외부 의존성이 없습니다. OpenZeppelin같은 외부 라이브러리를 import할 필요가 없어서 라이브러리에 버그가 생기거나 업데이트가 필요할 때 영향을 받지 않습니다. +- CEI 패턴의 단점은 개발자가 실수를 할 수도 있다는 것입니다. 2016년 The DAO 해킹 사건이 이의 대표적인 예시입니다. 하지만 ReentrancyGuard는 이러한 실수에서 자유롭습니다. nonReentrant modifier 하나만 붙이면 끝이여서 내부 로직 순서와 무관하게 재진입 자체를 원천 차단합니다. + +--- + +## 문제 10: [다이어그램] 재진입 공격 흐름 해석 (다이어그램 분석) + +다음 재진입 공격 시퀀스 다이어그램을 분석하세요: + +```mermaid +sequenceDiagram + participant A as 공격자 + participant V as VulnerableVault + + Note over A,V: 초기 상태: Vault 잔액 10 ETH, 공격자 예치금 1 ETH + + A->>V: 1. withdraw(1 ether) 호출 + V->>V: 2. require 통과 (잔액 1 ETH >= 1 ETH) + V->>A: 3. call{value: 1 ether}() - ETH 전송 + Note over A: 4. receive() 트리거됨 + A->>V: 5. receive()에서 다시 withdraw(1 ether) 호출 + V->>V: 6. require 통과 (잔액 아직 1 ETH!) + V->>A: 7. 또 1 ETH 전송 + Note over A: 8. 반복... + Note over V: 9. Vault 잔액 0이 될 때까지 반복 + V->>V: 10. 최종: balances[attacker] -= 1 ether (여러 번 실행됨) +``` + +**질문 1:** 6번에서 require 체크가 통과하는 이유는 무엇인가요? + +**답변:** + +balances[msg.sender] -= amount가 call 이후에 있기 때문입니다. 3번에서 ETH를 전송했지만 잔액은 아직 차감되지 않았으므로 5번에서 다시 withdraw를 호출했을 때 balances[attacker]는 여전히 1 ETH입니다. 그래서 6번의 require(1 ether >= 1 ether)가 통과됩니다. + +**질문 2:** CEI 패턴을 적용하면 6번에서 어떻게 되나요? + +**답변:** + +CEI 패턴에서는 2번 require 통과 직후 balances[attacker]를 0으로 먼저 차감합니다. 그러면 5번에서 다시 withdraw를 호출할 때 6번의 require(0 >= 1 ether)가 false가 되어 즉시 revert됩니다. 재진입 시도가 첫 번째에서 차단됩니다. + +**질문 3:** 공격자가 총 몇 ETH를 탈취할 수 있나요? (예치금 1 ETH, Vault 총 잔액 10 ETH 가정) + +**답변:** + +공격자 예치금: 1 ETH +Vault 총 잔액: 10 ETH (자신의 1 ETH + 타인의 9 ETH) + +재진입 반복: +1회: 1 ETH 전송 (Vault 잔액 9 ETH, balances 아직 1 ETH) +2회: 1 ETH 전송 (Vault 잔액 8 ETH) +... +10회: 1 ETH 전송 (Vault 잔액 0 ETH) + +총 탈취: 10 ETH (자기 1 ETH + 타인 9 ETH) +공격자는 1 ETH를 입금하고 10 ETH를 탈취합니다 + +--- + +## 자기 평가 + +모든 문제를 풀었다면, 아래 체크리스트로 자기 평가를 해보세요: + +- [x] EVM의 결정론적 실행 필요성을 이해했다 +- [x] Storage/Memory/Stack의 차이와 비용을 알고 있다 +- [x] 재진입 공격의 원리를 설명할 수 있다 +- [x] CEI 패턴으로 재진입 공격을 방어할 수 있다 +- [x] tx.origin vs msg.sender의 보안 차이를 알고 있다 +- [x] ReentrancyGuard를 적용할 수 있다 + +--- + +## 참고 자료 + +- 이론: `eth-materials/week-03/theory/slides.md` +- 취약한 코드: `eth-homework/week-03/dev/src/Vault.sol` +- 안전한 코드: `eth-homework/week-03/dev/src/VaultSecure.sol` +- 테스트: `eth-homework/week-03/dev/test/Vault.t.sol` +- 용어: `eth-materials/resources/glossary.md` diff --git a/week-03/quiz/quiz-03.md b/week-03/quiz/quiz-03.md index d60b5e7..5930baa 100644 --- a/week-03/quiz/quiz-03.md +++ b/week-03/quiz/quiz-03.md @@ -26,6 +26,7 @@ D) 개발자가 코드를 디버깅하기 쉽게 하기 위해 +이렇게 작성 ---