From 208064494b80a0a46230195f83a4967f58265d6e Mon Sep 17 00:00:00 2001 From: SH Kwon Date: Wed, 11 Feb 2026 09:03:54 +0900 Subject: [PATCH 1/3] feat(week-01): complete counter assignment --- week-01/dev/src/Counter.sol | 4 + week-01/quiz/quiz-01-solution.md | 297 +++++++++++++++++++++++++++++ week-01/theory/quiz-01-solution.md | 67 +++++++ 3 files changed, 368 insertions(+) create mode 100644 week-01/quiz/quiz-01-solution.md 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..cacca2c 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 감소시킵니다 @@ -40,6 +41,8 @@ contract Counter { // TODO: count를 1 감소시키세요. 단, count가 0이면 revert해야 합니다. // 힌트: require(조건, "에러 메시지"); 를 사용하세요 // 힌트: 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/quiz/quiz-01-solution.md b/week-01/quiz/quiz-01-solution.md new file mode 100644 index 0000000..f4e1c59 --- /dev/null +++ b/week-01/quiz/quiz-01-solution.md @@ -0,0 +1,297 @@ +# Week 1 퀴즈: State/Account + Solidity 기초 + +**제출 방법:** +1. 이 파일을 복사하여 `quiz-01-solution.md`로 저장 +2. 각 문제에 답변 작성 (왜 그런지 설명 포함) +3. Pull Request 생성 (`quiz_submission` 템플릿 사용) + +**평가 기준:** +- 정답 여부보다 **개념 이해도**를 중점 평가합니다 +- "왜"에 대한 설명이 충분한지 확인합니다 +- 문법 오류는 크게 감점하지 않습니다 + +--- + +## 문제 1: [이론] 상태 머신 (객관식) + +이더리움에서 "상태 전이가 원자적(atomic)이다"라는 말의 의미를 가장 잘 설명한 것은? + +다음 상황을 고려하세요: + +``` +Alice가 Bob에게 1 ETH를 보내는 트랜잭션을 실행합니다. +중간에 가스가 부족해져서 트랜잭션이 실패했습니다. +``` + +**보기:** +A) Alice의 잔액만 감소하고 Bob의 잔액은 변하지 않는다 +B) Alice의 잔액과 Bob의 잔액 모두 변하지 않고, 가스비만 소모된다 +C) 네트워크가 자동으로 부족한 가스를 보충해서 트랜잭션을 완료한다 +D) 트랜잭션이 절반만 실행되어 0.5 ETH만 전송된다 + +**답변:** + +B) / Tx Execution이 끝까지 성공하면 State가 S -> S'이 된다. 이 State 전이가 원자적이기 때문에 Tx이 실패하니 Execution에 가스비가 소모되고 상태변화는 없으니 State는 유지된다 + +--- + +## 문제 2: [이론] 결정론적 실행 (객관식) + +이더리움 EVM이 "결정론적(deterministic)"으로 실행된다는 것의 핵심 이유는 무엇인가요? + +**보기:** +A) 모든 노드가 같은 하드웨어를 사용해야 해서 +B) 같은 입력(트랜잭션)이 주어지면 모든 노드가 같은 결과(상태)를 도출해야 하므로 +C) 중앙 서버가 모든 계산을 수행하고 결과를 배포해서 +D) 트랜잭션이 항상 1초 안에 처리되어야 해서 + +**답변:** + +B) / 같은 입력에 대해 모든 노드들은 항상 같은 State를 계산해야 하므로 EVM은 결정론적으로 Execute한다. 결정론적이지 않으면 노드마다 서로 다른 결과를 내어 합의를 할 수 있고, 체인이 포크되거나 할 수 있다. + +--- + +## 문제 3: [이론] EOA vs CA (객관식) + +다음 중 EOA(Externally Owned Account)와 CA(Contract Account)의 차이를 올바르게 설명한 것은? + +**보기:** +A) EOA는 코드를 실행할 수 있고, CA는 코드를 실행할 수 없다 +B) EOA만 트랜잭션을 시작할 수 있고, CA는 EOA에 의해 호출될 때만 실행된다 +C) CA만 ETH를 보유할 수 있고, EOA는 ETH를 보유할 수 없다 +D) EOA와 CA는 동일한 기능을 가지며 이름만 다르다 + +**답변:** + + +B) / EOA만 Private Key를 소우하고 있기 때문에 Tx를 직접 실행할 수 있고, CA는 코드만 갖고있기 때문에 스스로 Tx를 Execute 할 수 없어서 다른 CA나 EOA에 의해 호출될 때만 Execute된다 +CA는 Private Key가 없어 서명이 불가하다. 그래서 Tx를 스스로 시작할 수 없고 항상 외부에서 들어오는 메세지에 반응하는 형태로만 동작한다. + + +--- + +## 문제 4: [이론] 계정 상태 필드 (객관식) + +이더리움 계정 상태의 4가지 필드 중 `nonce`의 역할을 올바르게 설명한 것은? + +다음 상황을 고려하세요: + +``` +Alice의 현재 nonce: 5 +Alice가 두 개의 트랜잭션을 동시에 전송합니다: +- TX-A: nonce=5, Bob에게 1 ETH 전송 +- TX-B: nonce=5, Charlie에게 2 ETH 전송 +``` + +**보기:** +A) 두 트랜잭션 모두 성공적으로 처리된다 +B) TX-A만 처리되고 TX-B는 무시된다 (또는 그 반대) +C) 두 트랜잭션 모두 실패하고 Alice의 자산이 동결된다 +D) 네트워크가 자동으로 TX-B의 nonce를 6으로 변경한다 + +**답변:** + + +B) Ethereum에서의 Nonce는 EOA별로 Re-Execution Attack과 Double-Spending 방지를 위해 Tx Count를 Nonce로 기록한다. 그래서 Nonce = 5에 대해서는 Tx A 혹은 B만 Execution된다. + +--- + +## 문제 5: [이론] World State (객관식) + +World State에 대한 설명 중 올바른 것은? + +**보기:** +A) World State는 최신 100개 블록의 트랜잭션만 저장한다 +B) World State는 모든 계정의 현재 상태(주소 -> 상태 매핑)를 나타낸다 +C) World State는 EOA의 정보만 저장하고 CA 정보는 별도로 관리한다 +D) World State는 각 노드마다 다른 값을 가질 수 있다 + +**답변:** + + +B) / World State는 Ethereum 네트워크에 존재하는 모든 계정(EOA + CA)에 대한 현재 State를 담고있는 Global State다. 전화번호부의 mapping은 (이름 : 전화번호) 형태이고 이에 빗대어 World State는 (Account : Current State) 형태로 되어있다. + +--- + +## 문제 6: [이론] 상태 변수 vs 지역 변수 (단답형) + +Solidity에서 `상태 변수(state variable)`와 `지역 변수(local variable)`의 차이는 무엇인가요? + +다음 코드를 보고 설명하세요: + +```solidity +contract Example { + uint256 public count; // 이것은 무엇인가요? + + function calculate(uint256 input) public pure returns (uint256) { + uint256 result = input * 2; // 이것은 무엇인가요? + return result; + } +} +``` + +**답변:** + + +1. 상태변수는 Storage(블록체인 영구 저장)에 지역변수는 Memory(임시 메모리)에 저장된다 +2. 상태 변수는 영원히 체인위에 기록, 지역변수는 함수 실행 단위로 생성 및 삭제된다 +3. 상태 변수가 지역 변수보다 비용이 훨씬 많이 든다 + +--- + +## 문제 7: [이론] 원자성의 이유 (단답형) + +이더리움에서 트랜잭션이 "원자적(atomic)"으로 처리되어야 하는 이유는 무엇인가요? + +**왜** 부분적으로 성공하는 트랜잭션을 허용하면 문제가 될까요? 구체적인 예시와 함께 설명하세요. + +**답변:** + + +트랜잭션은 원자적으로 처리되어야 State 변화가 전부 처리되거나 혹은 롤백된다. 송금 트랜잭션을 실행하는데 송금자의 잔액이 줄고 수신자의 잔액이 늘지 않았다면 State 불일치가 발생하기 때문에 네트워크 전체 합의가 깨지는 문제가 발생한다 + +--- + +## 문제 8: [이론] 계정 구조 설명 (단답형) + +EOA에는 `codeHash`와 `storageRoot`가 왜 의미가 없나요? + +**답변:** + + +EOA에는 코드가 없고 State Variable을 저장하지 않는다. 그렇기 때문에 codeHash, storageRoot 모두 빈 값이다. CA는 Code, State Variable이 있기 때문에 모두 가진다. + +--- + +## 문제 9: [코드] Counter 읽기 (코드 읽기) + +다음 Counter.sol 코드를 분석하세요: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +contract Counter { + uint256 public count; + + function getCount() public view returns (uint256) { + return count; + } + + function increment() public { + count += 1; + } + + function decrement() public { + require(count > 0, "Count cannot go below zero"); + count -= 1; + } +} +``` + +**1) `public` 키워드의 역할:** +`count` 변수에 `public`이 붙으면 어떤 일이 자동으로 일어나나요? + +**답변:** + + +Compiler가 getter()를 자동으로 생성해서 외부에서 해당 State Variable를 읽을 수 있도록 한다. +그래서 별도의 read function을 생성하지 않아도 된다. + + +**2) `view` 키워드의 의미:** +`getCount()` 함수에 `view`가 붙은 이유는 무엇인가요? `view`를 제거하면 어떻게 될까요? + +**답변:** + + +읽기 전용을 사용하는 키워드. 단순 State Variable read를 위함인데 view가 없으면 Tx로 처리돼서 Gas 비용이 낭비가 된다. + +--- + +## 문제 10: [코드] Counter 동작 예측 (코드 읽기) + +위의 Counter 컨트랙트에서 다음 시나리오를 분석하세요: + +**시나리오:** +``` +초기 상태: count = 0 + +1. increment() 호출 +2. increment() 호출 +3. decrement() 호출 +4. decrement() 호출 +5. decrement() 호출 +``` + +**질문 1:** 5번째 `decrement()` 호출의 결과는 무엇인가요? + +**답변:** + + +4번 함수 실행이 끝났을 때 count의 값은 0이다 +그리고 5번함수가 실행되면 require(count > 0, ...) 조건에 걸리기 때문에 +Tx이 Revert되고 전체 롤백되므로 count는 0이다 + + +**질문 2:** 왜 `decrement()` 함수에 `require(count > 0, ...)` 조건이 필요한가요? + +**답변:** + + +uint256의 정수 범위는 0~2^256 - 1이기 때문에 자료형 문제로 Underflow같은 현상이 발생할 수 있다 + +--- + +## 자기 평가 + +모든 문제를 풀었다면, 아래 체크리스트로 자기 평가를 해보세요: + +- [x] 상태 머신과 원자성 개념을 이해했다 +- [x] EOA와 CA의 차이를 설명할 수 있다 +- [x] 계정 상태의 4가지 필드(nonce, balance, storageRoot, codeHash)를 이해했다 +- [x] Solidity 기본 문법(public, view, require)을 이해했다 +- [x] 상태 변수와 지역 변수의 차이를 설명할 수 있다 + +--- + +## 참고 자료 + +- 이론: `eth-materials/week-01/theory/slides.md` +- 코드: `eth-homework/week-01/dev/src/Counter.sol` +- 용어: `eth-materials/resources/glossary.md` diff --git a/week-01/theory/quiz-01-solution.md b/week-01/theory/quiz-01-solution.md new file mode 100644 index 0000000..84ddfa5 --- /dev/null +++ b/week-01/theory/quiz-01-solution.md @@ -0,0 +1,67 @@ +# Week 1 이론 퀴즈 + +이 퀴즈를 복사하여 `quiz-01-solution.md`로 저장한 후 답변을 작성하세요. + +--- + +## 문제 1: 블록체인 기초 + +블록체인의 핵심 목적은 무엇인가요? 중앙화된 데이터베이스와 비교하여 설명해주세요. + +**답변:** +블록체인의 핵심 목적은 중개자 없는 당사자 끼리의 직접적인 거래를 신뢰할 수 있는 분산 원장을 위한 수단입니다. +보통은 일상은 클라이언트와 중앙화 데이터베이스간의 통신으로 서비스를 이용하지만, 블록체인을 통한 거래는 직접 사람대 사람으로 중개자 없이 성사된다. + +--- + +## 문제 2: 이더리움의 특징 + +이더리움이 비트코인과 다른 점은 무엇인가요? 스마트 컨트랙트의 관점에서 설명해주세요. + +**답변:** +비트코인은 암호화폐로써의 목적으로 등장했지만, 이더리움은 플랫폼적인 성향을 갖고 등장해서 가장 큰 차이는 코드 실행이다. +그래서 Turing-Complete Platform으로 불린다. EVM을 통해 이더리움 위에서 코드를 실행하고 할 수 있다. + +--- + +## 문제 3: Solidity 기초 + +다음 Solidity 코드에서 `public`과 `view` 키워드의 의미를 각각 설명하세요. + +```solidity +function count() public view returns (uint256) { + return _count; +} +``` + +**답변:** +- `public`: 누구나 함수 호출이 가능하다. (외부, 내부, 상속) +- `view`: 읽기 전용 함수로, State Variable를 읽기만 하고 Gas 무료다. + +--- + +## 문제 4: 상태 변수 + +Solidity에서 상태 변수(state variable)와 지역 변수(local variable)의 차이점을 설명하세요. + +**답변:** +State Variable은 실제 블록체인에 영구저장되고, Storage에 저장하며 Gas가 비싸다 +Local Variable은 함수 실행 중에만 Memory에 존재하는 임시값이다 + +--- + +## 문제 5: Gas 개념 + +이더리움에서 Gas란 무엇이며, 왜 필요한가요? + +**답변:** +Gas는 Ethereum에서 Tx, Contract Execution에 필요한 연산량을 측정하는 단위다. +무한루프 등과 같은 자원 악용을 막고, 복잡할수록 더 많은 Gas를 지불하도록 설계돼 Validator들에게 공정한 보상을 제공한다. + +--- + +**제출 방법:** +1. 이 파일을 복사하여 `quiz-01-solution.md`로 저장 +2. 각 문제에 대한 답변 작성 +3. Git으로 커밋 및 푸시 +4. Pull Request 생성 From 766ae284652fc4094166600625d80666e223b867 Mon Sep 17 00:00:00 2001 From: SH Kwon Date: Tue, 17 Feb 2026 00:46:31 +0900 Subject: [PATCH 2/3] feat(week-02): complete counter assignment --- week-02/dev/src/SimpleStorage.sol | 7 + week-02/quiz/quiz-02-solution.md | 340 ++++++++++++++++++++++++++++++ 2 files changed, 347 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..25a6078 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,10 @@ 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..8d04152 --- /dev/null +++ b/week-02/quiz/quiz-02-solution.md @@ -0,0 +1,340 @@ +# 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이 높을수록 트랜잭션이 빨리 처리된다 + +**답변:** + +B) / 정답대로 gasPrice는 단위당 가격이고 gasLimit은 최대 사용량이다 +실제 사용한 가스 양은 gasUsed이다. 그래서 (총 가스 비용) = (gasUsed) * (gasPrice) + +--- + +## 문제 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의 계정이 잠긴다 + +**답변:** + + +B) / Nonce가 우선이다, gasPrice Tx-B가 더 높더라도 nonce가 뒷순이기 때문에 Tx-A가 우선이다 +같은 계정에서 안에서 nonce 순서대로만 트랜잭션을 확정할 수 있다. + +--- + +## 문제 3: [이론] 디지털 서명 (객관식) + +디지털 서명(ECDSA)이 보장하는 세 가지 속성 중, "누군가 내 트랜잭션을 위조할 수 없다"를 보장하는 것은? + +**보기:** +A) 인증 (Authentication) +B) 무결성 (Integrity) +C) 부인 방지 (Non-repudiation) +D) 암호화 (Encryption) + +**답변:** + + +A) +Authentication : 서명 검증 시 해당 트랜잭션은 이 Public Key의 Private Key로만 만들 수 있다를 보장해서, 다른 사람이 위조 서명을 만들 수 없도록 한다. +Integrity : 서명 데이터가 한 비트라도 바뀌면 서명이 더 이상 유효하지 않으므로, 전송 중 조작/변경을 탐지할 수 있다 +Non-repudiation : 나중에 내가 해당 트랜잭션을 안 보냈다라고 주장해도, 유효한 서명이 남아 있는 한 본인이 보냈음을 부인할 수 없게 만드는 성질이다 + +--- + +## 문제 4: [이론] 키 유도 (객관식) + +다음 중 키 유도 과정에서 올바른 방향을 설명한 것은? + +**보기:** +A) Public Key -> Private Key -> Address 순으로 유도된다 +B) Address -> Public Key -> Private Key 순으로 역추적 가능하다 +C) Private Key -> Public Key -> Address 순으로 유도되며, 역방향은 불가능하다 +D) 세 값은 독립적으로 생성되며 서로 연관이 없다 + +**답변:** + + +C) / Private Key 는 랜덤으로 256bit로 주어진다. 이를 ECDSA secp256k1 곡선 연산에 의해 Public Key를 생성한다(512bits, 65bytes). 이 Public Key를 Keccak256 Hash를하고 뒤 20bytes를 통해 Address를 생성해내서 +총 큰 연산 2회(ECDSA, Keccak256 Hash) 모두 역연산 불가능이기 때문에 불가능하다. + +그래서 Address만 보고 Private Key를 찾아내려고 해도 불가능하다 + +--- + +## 문제 5: [이론] nonce의 필요성 (단답형) + +이더리움에서 **왜** nonce가 필요한가요? + +만약 nonce가 없다면 어떤 공격이 가능해질까요? 구체적인 예시와 함께 설명하세요. + +**답변:** + + +Nonce는 같은 계정에서 동일한 트랜잭션이 Replay Attack되는 것을 방지하기 위해 필요하다. +Nonce가 없다면 공격자는 예를들어 Alice의 서명된 "Bob에게 1ETH 송금" 트랜잭션을 여러 번 테느워크에 재전송 할 수 있고, Alice의 계좌에서 1ETH씩 무한 인출되는 재앙이 발생할 수 있다. +각 트랜잭션마다 nonce가 증가함으로써 이미 처리된 트랜잭션은 무효화돼 일회성 Execution만 보장한다. + +--- + +## 문제 6: [이론] Private Key 보안 (단답형) + +2022년 Ronin Bridge 해킹에서 약 $625M이 탈취되었습니다. + +**왜** Private Key 유출이 이렇게 치명적인가요? 은행 계좌 비밀번호 유출과 비교해서 설명하세요. + +**답변:** + + +은행 계좌 비밀번호 유출 시 은행이 이상거래를 감지하고 즉시 계좌 동결/취소가 가능하지만, +블록체인에서는 Private Key로 서명된 트랜잭션이 블록에 들어가서 확정되면 immutable하므로 복구 불가능하다. + +Private Key 유출은 블록체인에서 계좌 전체 자산이 즉시 탈취될 수 있는 심각한 보안 사고다. + +--- + +## 문제 7: [이론] EIP-1559 이해 (단답형) + +EIP-1559 이전과 이후의 가스 수수료 메커니즘의 가장 큰 차이점은 무엇인가요? + +**힌트:** `baseFee`와 `priorityFee`의 역할을 설명하면서 답변하세요. + +**답변:** + +컨셉적으로는 경매식 고정 가격 -> 자동조정 baseFee + 팁 구조로 전환됐다 + +이전 총 수수료 = gasUsed * gasPrice +gasPrice는 사용자가 직접 입찰하고, 문제는 예측 불가하며 과납부가 빈번했다 + +그래서 현재는 gasUsed * (baseFee * priorityFee)로 계산된다 +baseFee는 네트워크가 현재 블록 혼잡도에 따라 자동 조정한다 +priorityFee는 사용자 설정 팁이다, Validator 우선순위용이다 +baseFee는 burn하고 ETH 디플레이션 효과가 난다 + +--- + +## 문제 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:1ether}(); // TODO: 1 ether를 입금하는 코드 + + // Assert: 잔액 확인 + assertEq(storage_.getBalance(user), 1ether); // TODO: 예상 잔액 + } +} +``` + +**답변:** +```solidity +// 빈칸을 채운 완성 코드를 작성하세요 +``` + +**왜 이렇게 작성했나요:** + + +vm.prank() : 현재 msg.sender를 user로 위장한다 +deposit{value:1ether}() : payable 함수에 ETH 전송 +assertEq(..., 1ether) : 입금한 만큼 잔액 증가 확인 + +--- + +## 문제 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) 왜 이것이 문제인가:** + +잔액이 음수값이되는 언더플로우가 발생한다. +솔리디티 0.8 이전에는 이 문제가 해결이 안돼서 잔액이 양수로 높게잡히는 취약점이 있었다. + + +**3) 올바른 수정 방법:** +```solidity +// GOOD CODE - 수정된 withdraw 함수를 작성하세요 + +function withdraw(uint256 amount) public { + require(amount <= balances[msg.sender], "Insufficient Balance"); + // 잔액 차감 + balances[msg.sender] -= amount; + + // ETH 전송 + 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:** 이 테스트가 실패하는 이유는 무엇인가요? + +**답변:** + +잔액이 0인데, 출금을 시도한다 그래서 revert된다 + + +**질문 2:** 이 테스트를 "출금 실패를 테스트하는 정상 테스트"로 바꾸려면 어떻게 수정해야 하나요? + +**답변:** +```solidity +// 힌트: vm.expectRevert를 사용하세요 +// 수정된 테스트 코드를 작성하세요 + function test_WithdrawFails() public { + vm.expectRevert("Insufficient Balance"); + + 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 4967ad7fafc43078dfe4bfb7ab2c723168e7f85e Mon Sep 17 00:00:00 2001 From: SH Kwon Date: Thu, 5 Mar 2026 09:25:10 +0900 Subject: [PATCH 3/3] feat(week-03): complete counter assignment --- week-03/dev/src/Vault.sol | 4 +- week-03/dev/src/VaultSecure.sol | 18 +- week-03/dev/test/Vault.t.sol | 9 +- week-03/quiz/quiz-03-solution.md | 426 +++++++++++++++++++++++++++++++ 4 files changed, 449 insertions(+), 8 deletions(-) create mode 100644 week-03/quiz/quiz-03-solution.md diff --git a/week-03/dev/src/Vault.sol b/week-03/dev/src/Vault.sol index 7ce2e5f..bf3fb87 100644 --- a/week-03/dev/src/Vault.sol +++ b/week-03/dev/src/Vault.sol @@ -107,7 +107,9 @@ contract Vault { // ======================================== // 이 줄에 도달하기 전에 위의 call()에서 재진입이 발생하면 // 공격자는 balances가 업데이트되기 전에 반복 출금 가능 - balances[msg.sender] -= amount; + unchecked { + balances[msg.sender] -= amount; + } // 출금 이벤트 발생 emit Withdrawn(msg.sender, amount); diff --git a/week-03/dev/src/VaultSecure.sol b/week-03/dev/src/VaultSecure.sol index a9a19e3..518d889 100644 --- a/week-03/dev/src/VaultSecure.sol +++ b/week-03/dev/src/VaultSecure.sol @@ -75,7 +75,7 @@ pragma solidity 0.8.26; // import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /// @dev ReentrancyGuard 사용 시: contract VaultSecure is ReentrancyGuard -contract VaultSecure { +contract VaultSecure{ // ============================================ // 상태 변수 // ============================================ @@ -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를 출금합니다 @@ -125,7 +126,18 @@ contract VaultSecure { /// CEI 패턴 사용 시 순서: Checks -> Effects -> Interactions /// ReentrancyGuard 사용 시: nonReentrant modifier 추가 function withdraw(uint256 amount) public { - // TODO: 구현하세요 + // 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"); + + // Events + emit Withdrawn(msg.sender, amount); } // ============================================ diff --git a/week-03/dev/test/Vault.t.sol b/week-03/dev/test/Vault.t.sol index 46cb50b..356a982 100644 --- a/week-03/dev/test/Vault.t.sol +++ b/week-03/dev/test/Vault.t.sol @@ -58,7 +58,7 @@ contract Attacker { if (attackCount < 5 && address(vault).balance >= attackAmount) { attackCount++; console.log("Reentrancy attempt:", attackCount); - vault.withdraw(attackAmount); + try vault.withdraw(attackAmount) {} catch {} } } @@ -310,10 +310,11 @@ contract VaultSecureTest is Test { ); // Attacker는 자신이 입금한 1 ETH만 돌려받음 - // 10 ETH (초기) - 1 ETH (입금) + 1 ETH (정상 출금) = 10 ETH + // 10 ETH (초기) + 1 ETH (테스트에서 전송) = 11 ETH + // attack()에서 1 ETH 입금 후 1 ETH 정상 출금하므로 순 변화 없음 assertEq( address(attacker).balance, - attackerBalanceBefore, + attackerBalanceBefore + 1 ether, "Attacker should only get back their deposited amount" ); @@ -424,7 +425,7 @@ contract AttackerForVault { receive() external payable { if (attackCount < 10 && address(vault).balance >= 1 ether) { attackCount++; - vault.withdraw(1 ether); + try vault.withdraw(1 ether) {} catch {} } } } diff --git a/week-03/quiz/quiz-03-solution.md b/week-03/quiz/quiz-03-solution.md new file mode 100644 index 0000000..055a60c --- /dev/null +++ b/week-03/quiz/quiz-03-solution.md @@ -0,0 +1,426 @@ +# 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) / EVM은 모든 노드가 동일한 바이트코드를 같은 입력으로 실행했을 때 결과가 달라지지 않아야 하므로, 실행 중에 결과가 달라질 수 있는 랜덤/외부 입출력 같은 비결정론적 요소를 허용하지 않는다. + +--- + +## 문제 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) +data는 memory 키워드를 사용해서 메모리 영역에 올라가는 동적 배열이다 +함수 호출 시에만 존재하고, 함수가 끝나면 사라진다 +가스비는 높은순대로 Storage, Memory, Stack 이렇게 된다 + +Storage는 블록체인 State가 저장되는 영구 저장소다 모든 노드가 디스크에 기록/갱신해서 비싸다 +Memory는 함수 실행 동안만 존재하는 일시적인 저장소이므로 읽고 쓰는 비용이 있지만, Storage에 비하면 훨씬 저렴하다 +Stack은 EVM 연산이 직접 사용하는 256비트 word stack이라서 매우 빠르고 가스비도 저렴하다 + + +--- + +## 문제 3: [이론] Gas 비용 (객관식) + +다음 중 Gas 비용이 가장 높은 연산은? + +**보기:** +A) ADD (덧셈) +B) MUL (곱셈) +C) SLOAD (Storage 읽기) +D) SSTORE (Storage 쓰기) + +**답변:** + + +D) +SSTORE 연산은 컨트랙트의 Storage state를 실제로 변경하는 연산이다 +Storage는 블록체인에 영구 저장되는 데이터 영역이라, 한 번 쓰면 전 세계 모든 노드가 그 변경 내용을 자신의 State에 반영해야 한다 + +--- + +## 문제 4: [이론] CEI 패턴 (단답형) + +**왜** CEI(Checks-Effects-Interactions) 패턴에서 Effects(상태 변경)가 Interactions(외부 호출)보다 먼저 와야 하나요? + +재진입 공격 시나리오와 연결해서 구체적으로 설명하세요. + +**답변:** + + +Interactions을 먼저 하면 외부 컨트랙트의 fallback/receive 코드가 실행되면서 아직 상태가 갱신되지 읺은 시점에 다시 같은 함수를 호출해 여러 번 출금하는 재진입 공격이 가능하다 +Effects를 먼저 해서 State를 변경된 State로 만들어 두면, 그 다음에 interactions에서 외부 컨트랙트를 호출해도 재진입 시 공격이 불가능하다 + +--- + +## 문제 5: [이론] The DAO 사건 교훈 (단답형) + +2016년 The DAO 해킹($60M 피해)에서 우리가 배워야 할 **가장 중요한 교훈**은 무엇인가요? + +이 사건 이후 이더리움 생태계에 어떤 변화가 있었나요? + +**답변:** + + +스마트 컨트랙트는 곧 돈이다, 그래서 배포 전 충분한 검증/감사/보언 설계 없이 복잡한 코드를 메인넷에 올리면 안된다. +이 사건 이후로 이더리움 생태계에서는 보안 감사, 버그 바운티, 재진입 방어 패턴이 크게 강조됐고, +동시에 하드포크를 둘러싼 논쟁을 계기로 코드가 법인지, 커뮤니티 합의가 우선인지 철학적 논의 속에서 이더리움 / 이더리움 클래식으로 분기되는 변화를 겪었다. + + +--- + +## 문제 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()는 재진입성 취약점이있다. 외부 호출후에 잔액을 차감한다. +transfer 후 잔액 차감 순서가 잘못됐다. +두 순서가 바뀌어야한다 + + +**2) 왜 이것이 문제인가:** + + +악의적인 컨트랙트를 통해 deposit()으로 일부 ETH를 넣고 withdraw()를 호출한다 +msg.sender.call(value: amount)("")가 실행되면, 공격자 컨트랙트의 fallback/receive 함수가 실행되고, 그 안에서 다시 withdraw를 재호출한다 +아직 잔액 차감이 되지 않았기 때문에 공격 하려는 vault에 있는 ETH를 거의 빼낼 수 있다 + +**3) 올바른 수정 방법 (CEI 패턴):** +```solidity +// GOOD CODE - CEI 패턴으로 수정하세요 +function withdraw(uint256 amount) public { + 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"); +} +``` + +**왜 이 순서가 중요한가요:** + + +재진입 공격이 발생해도, 즉 withdraw() 재호출 시 balances가 require()에 막혀서 추가 출금이 불가능해진다 + +--- + +## 문제 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) 발견한 취약점:** + +msg.sender는 현재 함수를 호출한 주소이고, tx.origin은 트랜잭션을 처음 생성하고 서명한 EOA 주소다 + +정상 컨트랙트 배포자가 owner인 상태인데, 공격자가 피싱 컨트랙트 생성해서 정상인이 피싱사이트에서 실수로 Tx를 서명했다면 여기서 tx.origin도 정상인 owner도 정상인이 된다. +그러면 새 owner는 피싱 컨트랙트로 변경될 수 있는 취약점이 있다 + + +**2) 공격 시나리오:** + + +공격자가 악성 컨트랙트를 배포하고 소유주가 이 악성 컨트랙트로 ETH를 전송한다 +악성 컨트랙트의 receive()함수를 자동 실행시켜 receive()에서 PhishingVulnerable.transferOwnership()를 호출한다 + + +**3) 올바른 수정 방법:** +```solidity +contract PhishingVulnerable { + address public owner; + + constructor() { + owner = msg.sender; + } + + function transferOwnership(address newOwner) public { + require(tx.origin == msg.sender, "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/security/ReentrancyGuard.sol"; + +contract SecureVault is ReentracyGuard{ + mapping(address => uint256) public balances; + + function deposit() public payable { + balances[msg.sender] += msg.value; + } + + 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 코드 순서로 방어해서 가장 저렴하지만 단일 함수정도만 보호되기 때문에 복잡한 로직에서 순서 지키기가 어렵다 +외부 의존성을 사용하면 추가 가스비용이 들지만 한 번 설정하면 모든 함수 보호가 가능하고 실수 방지도 가능하다 + +--- + +## 문제 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 체크가 통과하는 이유는 무엇인가요? + +**답변:** + +계속 출금이 반복돼 공격자의 1ETH는 아직 차감되지 않았기 때문이다 +Vault 내 잔액이 require()를 통과하지 못하는 시점에서 상태 변경이 일어난다 + + +**질문 2:** CEI 패턴을 적용하면 6번에서 어떻게 되나요? + +**답변:** + +6번에서 require()에 실패한다. 그래서 재진입 공격을 방어할 수 있다 + + +**질문 3:** 공격자가 총 몇 ETH를 탈취할 수 있나요? (예치금 1 ETH, Vault 총 잔액 10 ETH 가정) + +**답변:** + + +Vault 잔액 모두 (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`