diff --git a/week-01/dev/src/Counter.sol b/week-01/dev/src/Counter.sol index f9687e3a..4d0b3bcc 100644 --- a/week-01/dev/src/Counter.sol +++ b/week-01/dev/src/Counter.sol @@ -32,14 +32,14 @@ contract Counter { function increment() public { // TODO: count를 1 증가시키세요 // 힌트: count += 1; 또는 count = count + 1; 또는 count++; + count += 1; } /// @notice 카운트를 1 감소시킵니다 /// @dev count가 0일 때 감소시키면 언더플로우가 발생합니다 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"); + count -= 1; } /// @notice 카운트를 0으로 초기화합니다 @@ -47,5 +47,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 00000000..07945924 --- /dev/null +++ b/week-01/quiz/quiz-01-solution.md @@ -0,0 +1,253 @@ +# 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) "원자적(atomic)"이란 트랜잭션이 "전부 성공하거나, 전부 실패하거나" 둘 중 하나만 가능하다는 의미입니다. 중간 상태란 존재하지 않습니다. 가스 부족으로 트랜잭션이 실패하면, 트랜잭션이 시도한 모든 상태 변경(Alice 잔액 감소, Bob 잔액 증가)은 롤백됩니다. 다만 트랜잭션을 실행하려고 시도한 가스비(수수료)는 이미 검증자에게 지급되었으므로 소모됩니다. A는 "절반 성공"을 의미하므로 원자성에 위배되고, C와 D는 EVM의 동작 방식과 맞지 않습니다. + + +--- + +## 문제 2: [이론] 결정론적 실행 (객관식) + +이더리움 EVM이 "결정론적(deterministic)"으로 실행된다는 것의 핵심 이유는 무엇인가요? + +**보기:** +A) 모든 노드가 같은 하드웨어를 사용해야 해서 +B) 같은 입력(트랜잭션)이 주어지면 모든 노드가 같은 결과(상태)를 도출해야 하므로 +C) 중앙 서버가 모든 계산을 수행하고 결과를 배포해서 +D) 트랜잭션이 항상 1초 안에 처리되어야 해서 + +**답변:** +B) 이더리움은 탈중앙화 네트워크이므로 전 세계 수천 개의 노드가 독립적으로 트랜잭션을 실행합니다. 모든 노드가 같은 트랜잭션을 실행했을 때 같은 결과(상태)를 얻어야 합의(consensus)가 가능합니다. 만약 결정론적이지 않다면, 노드 A는 Alice의 잔액이 5 ETH라 계산하고 노드 B는 3 ETH라 계산하는 상황이 발생합니다. 이렇게 되면 네트워크가 하나의 "진실"에 합의할 수 없어 블록체인이 작동하지 않습니다. 이 때문에 EVM에서는 `block.timestamp`처럼 제한된 값만 허용하고, 랜덤 함수나 외부 API 호출 같은 비결정론적 연산은 금지됩니다. + + +--- + +## 문제 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(Externally Owned Account)는 개인 키(Private Key)를 가진 계정으로, 이 키로 트랜잭션에 서명하여 트랜잭션을 시작할 수 있습니다. 반면 CA(Contract Account)는 코드로 제어되며 자체적으로 트랜잭션을 시작할 수 없습니다. CA는 EOA가 보낸 트랜잭션이나 다른 컨트랙트의 호출에 의해서만 실행됩니다. CA가 트랜잭션을 시작할 수 없는 이유는 Private Key가 없기 때문입니다. 트랜잭션을 시작하려면 서명이 필요하고, 서명에는 Private Key가 필요한데 CA에는 Private Key가 존재하지 않습니다. + + +--- + +## 문제 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) nonce는 각 계정에서 보낸 트랜잭션의 일련번호입니다. 같은 nonce를 가진 두 트랜잭션이 있으면, 하나만 처리되고 나머지는 무효화됩니다. Alice의 현재 nonce가 5이므로, nonce=5인 TX-A 또는 TX-B 중 먼저 블록에 포함되는 하나만 처리됩니다. 처리 후 Alice의 nonce는 6이 되어, 나머지 nonce=5 트랜잭션은 이미 사용된 nonce이므로 거절됩니다. nonce는 "리플레이 공격(replay attack)"을 방지합니다. nonce가 없다면 공격자가 Alice의 서명된 트랜잭션을 복사하여 여러 번 브로드캐스트할 수 있고, 같은 전송이 반복 실행되어 Alice의 자산이 모두 소진될 수 있습니다. + + +--- + +## 문제 5: [이론] World State (객관식) + +World State에 대한 설명 중 올바른 것은? + +**보기:** +A) World State는 최신 100개 블록의 트랜잭션만 저장한다 +B) World State는 모든 계정의 현재 상태(주소 -> 상태 매핑)를 나타낸다 +C) World State는 EOA의 정보만 저장하고 CA 정보는 별도로 관리한다 +D) World State는 각 노드마다 다른 값을 가질 수 있다 + +**답변:** +B) World State는 이더리움 네트워크에 존재하는 모든 계정의 현재 상태를 담은 거대한 매핑(주소 → 계정 상태)입니다. "전화번호부"에 비유되는 이유는, 전화번호부가 이름(주소)으로 전화번호(상태)를 찾을 수 있는 것처럼, World State도 계정 주소를 키로 해서 해당 계정의 nonce, balance, storageRoot, codeHash 등 현재 상태를 조회할 수 있기 때문입니다. World State는 EOA와 CA를 모두 포함하며, 합의에 참여하는 모든 정상 노드는 같은 World 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; + } +} +``` + +**답변:** +- `count`는 **상태 변수(state variable)** 입니다. 컨트랙트 레벨에 선언되며 블록체인의 **Storage**에 영구적으로 저장됩니다. 트랜잭션이 종료된 후에도 값이 유지되며, 다음 트랜잭션에서도 그 값을 읽을 수 있습니다. Storage에 데이터를 쓰는 것은 SSTORE 연산으로 약 20,000 gas가 소모되어 매우 비쌉니다. + +- `result`는 **지역 변수(local variable)** 입니다. 함수 내부에 선언되며 **Memory**(또는 Stack)에 임시로 저장됩니다. 함수 실행이 끝나면 즉시 사라지며, 다음 호출에서는 존재하지 않습니다. Memory 연산은 Storage보다 훨씬 저렴합니다(약 3 gas). + +정리하면: + +| 구분 | 상태 변수 (count) | 지역 변수 (result) | +|------|-------------------|-------------------| +| 저장 위치 | Storage | Memory/Stack | +| 지속성 | 영구 (트랜잭션 간 유지) | 임시 (함수 종료 시 삭제) | +| 비용 | 비쌈 (~20,000 gas) | 저렴 (~3 gas) | + + +--- + +## 문제 7: [이론] 원자성의 이유 (단답형) + +이더리움에서 트랜잭션이 "원자적(atomic)"으로 처리되어야 하는 이유는 무엇인가요? + +**왜** 부분적으로 성공하는 트랜잭션을 허용하면 문제가 될까요? 구체적인 예시와 함께 설명하세요. + +**답변:** +원자성이 보장되지 않으면 시스템의 일관성(consistency)이 깨집니다. 예를 들어, Alice가 Bob에게 1 ETH를 전송하는 트랜잭션이 "절반만 성공"한다면, Alice의 잔액은 1 ETH 줄었지만 Bob의 잔액은 증가하지 않는 상황이 발생할 수 있습니다. 이 경우 1 ETH가 네트워크에서 사라지는 셈이 됩니다. DeFi에서는 더 심각합니다. 토큰 스왑 시 사용자의 토큰 A는 빠져나갔는데 토큰 B는 받지 못하면 자산이 영구적으로 손실됩니다. 원자성 덕분에 "전부 성공 아니면 전부 실패"로 처리되어, 실패 시 모든 상태가 원래대로 롤백되므로 이런 불일치가 절대 발생하지 않습니다. + + +--- + +## 문제 8: [이론] 계정 구조 설명 (단답형) + +EOA에는 `codeHash`와 `storageRoot`가 왜 의미가 없나요? + +**답변:** +EOA는 단순히 Private Key로 제어되는 "지갑" 계정으로, 스마트 컨트랙트 코드가 없습니다. `codeHash`는 계정에 배포된 스마트 컨트랙트 바이트코드의 해시인데, EOA에는 코드가 없으므로 빈 문자열의 해시(`keccak256("")`)로 고정됩니다. `storageRoot`는 컨트랙트의 상태 변수들이 저장된 Storage Trie의 루트 해시인데, EOA는 상태 변수를 가질 수 없으므로(코드가 없으니 변수를 선언하고 저장할 곳이 없음) 빈 Trie의 루트로 고정됩니다. 반면 CA는 코드가 있고 mapping, uint256 등의 상태 변수를 Storage에 저장하므로, `codeHash`와 `storageRoot`가 의미 있는 값을 가집니다. + + +--- + +## 문제 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`이 붙으면 어떤 일이 자동으로 일어나나요? + +**답변:** +`public`으로 선언된 상태 변수에는 Solidity 컴파일러가 자동으로 동일한 이름의 **getter 함수**를 생성합니다. 즉, `uint256 public count;`라고 선언하면 `function count() external view returns (uint256)` 함수가 자동으로 만들어집니다. 외부에서 `counter.count()`를 호출하면 현재 count 값을 읽을 수 있습니다. 그래서 사실 `getCount()` 함수는 `count()` getter와 동일한 기능을 하는 중복 함수입니다. + + +**2) `view` 키워드의 의미:** +`getCount()` 함수에 `view`가 붙은 이유는 무엇인가요? `view`를 제거하면 어떻게 될까요? + +**답변:** +`view`는 이 함수가 블록체인의 상태를 **읽기만** 하고 **수정하지 않는다**는 것을 명시합니다. `getCount()`는 count 값을 읽어서 반환할 뿐 변경하지 않으므로 `view`가 적합합니다. `view` 함수는 트랜잭션 없이 로컬에서 실행할 수 있어 **가스비가 들지 않습니다**(외부에서 직접 호출 시). `view`를 제거하면 컴파일은 되지만, 함수 호출 시 불필요하게 트랜잭션을 생성해야 하므로 가스비가 소모됩니다. 또한 컴파일러가 경고를 발생시킵니다. + + +--- + +## 문제 10: [코드] Counter 동작 예측 (코드 읽기) + +위의 Counter 컨트랙트에서 다음 시나리오를 분석하세요: + +**시나리오:** +``` +초기 상태: count = 0 + +1. increment() 호출 +2. increment() 호출 +3. decrement() 호출 +4. decrement() 호출 +5. decrement() 호출 +``` + +**질문 1:** 5번째 `decrement()` 호출의 결과는 무엇인가요? + +**답변:** +5번째 `decrement()` 호출은 **실패(revert)** 합니다. 1~4번 호출 후 count는 0 → 1 → 2 → 1 → 0 순으로 변합니다. 5번째에서 count가 0인 상태에서 `decrement()`를 호출하면, `require(count > 0, "Count cannot go below zero")` 조건을 충족하지 못하여 "Count cannot go below zero" 에러 메시지와 함께 트랜잭션이 revert됩니다. + + +**질문 2:** 왜 `decrement()` 함수에 `require(count > 0, ...)` 조건이 필요한가요? + +**답변:** +`count`의 타입은 `uint256`(부호 없는 256비트 정수)으로, 음수를 표현할 수 없습니다. Solidity 0.8 이전에는 0에서 1을 빼면 **언더플로우(underflow)**가 발생하여 `2^256 - 1`이라는 엄청나게 큰 값이 되어버리는 치명적인 버그가 발생했습니다. Solidity 0.8부터는 기본적으로 산술 오버플로우/언더플로우를 감지하여 자동으로 revert하지만, `require`를 명시적으로 사용하면 **사람이 읽을 수 있는 에러 메시지**를 제공할 수 있고, 코드의 의도를 명확히 드러내어 가독성과 디버깅 편의성이 향상됩니다. + + +--- + +## 자기 평가 + +모든 문제를 풀었다면, 아래 체크리스트로 자기 평가를 해보세요: + +- [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`