From 7c6ec1ee255c0803dc5a20727fc569bb7a003fcb Mon Sep 17 00:00:00 2001 From: jaesimin Date: Thu, 26 Feb 2026 20:22:34 +0900 Subject: [PATCH] feat : make VaultSecure solution, solve quiz --- .gitignore | 2 + week-03/dev/src/VaultSecure.sol | 17 +- week-03/theory/quiz-03-solution.md | 380 +++++++++++++++++++++++++++++ 3 files changed, 395 insertions(+), 4 deletions(-) create mode 100644 week-03/theory/quiz-03-solution.md diff --git a/.gitignore b/.gitignore index ff3bed2..88eae22 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ node_modules/ # npm package-lock.json + +lib \ No newline at end of file diff --git a/week-03/dev/src/VaultSecure.sol b/week-03/dev/src/VaultSecure.sol index a9a19e3..c4e55ec 100644 --- a/week-03/dev/src/VaultSecure.sol +++ b/week-03/dev/src/VaultSecure.sol @@ -72,10 +72,10 @@ pragma solidity 0.8.26; // ============================================ // OpenZeppelin 사용 시 주석 해제 // ============================================ -// import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /// @dev ReentrancyGuard 사용 시: contract VaultSecure is ReentrancyGuard -contract VaultSecure { +contract VaultSecure is ReentrancyGuard { // ============================================ // 상태 변수 // ============================================ @@ -106,7 +106,11 @@ 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 +128,13 @@ contract VaultSecure { /// /// CEI 패턴 사용 시 순서: Checks -> Effects -> Interactions /// ReentrancyGuard 사용 시: nonReentrant modifier 추가 - function withdraw(uint256 amount) public { + function withdraw(uint256 amount) public nonReentrant { // TODO: 구현하세요 + require(balances[msg.sender] >= amount, "Insufficient balance"); + balances[msg.sender] -= amount; + (bool success, ) = msg.sender.call{ value: amount }(""); + require(success, "Transfer failed"); + emit Withdrawn(msg.sender, amount); } // ============================================ diff --git a/week-03/theory/quiz-03-solution.md b/week-03/theory/quiz-03-solution.md new file mode 100644 index 0000000..88e0552 --- /dev/null +++ b/week-03/theory/quiz-03-solution.md @@ -0,0 +1,380 @@ +# 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, 랜덤함수는 모든 노드에서 동일한 값을 받아야한다는 결정론적 특성을 위배하기 때문이다. + +--- + +## 문제 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, Storage는 영구적으로 저장되므로 가장 비싸고, Memory는 함수 실행 중에만 사용되므로 저렴하다. Stack은 가장 빠르지만 저장 공간이 제한적이다. + +--- + +## 문제 3: [이론] Gas 비용 (객관식) + +다음 중 Gas 비용이 가장 높은 연산은? + +**보기:** +A) ADD (덧셈) +B) MUL (곱셈) +C) SLOAD (Storage 읽기) +D) SSTORE (Storage 쓰기) + +**답변:** + +D, Storage는 영구적으로 저장되므로 가장 비싸고, Memory는 함수 실행 중에만 사용되므로 저렴하다. Stack은 가장 빠르지만 저장 공간이 제한적이다. + +--- + +## 문제 4: [이론] CEI 패턴 (단답형) + +**왜** CEI(Checks-Effects-Interactions) 패턴에서 Effects(상태 변경)가 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) 발견한 취약점:** + +밸런스 상태 변경이 외부 호출보다 나중에 일어난다. 따라서, 재진입 공격시 추가적인 밸런스 탈취가 일어날 수 있다. + +**2) 왜 이것이 문제인가:** + +재진입 공격을 통해 상태가 변경되기 전 fallback 함수로 다시 호출하여 밸런스를 탈취할 수 있다. + +**3) 올바른 수정 방법 (CEI 패턴):** +```solidity +// GOOD CODE - CEI 패턴으로 수정하세요 +function withdraw(uint256 amount) public { + // 여기에 안전한 코드를 작성하세요 + require(balances[msg.sender] >= amount, "Insufficient balance"); + balances[msg.sender] -= amount; + (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"); +} +``` + +**왜 이 순서가 중요한가요:** + + +상태변경을 먼저하기 때문에 재진입 공격시 밸런스가 이미 차감되어 추가적인 밸런스 탈취가 불가능하다. +--- + +## 문제 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 은 트랜잭션 최초 실행자, msg.sender는 호출자이다. tx.origin 을 사용하면 안되는 취약점이 있다. + +**2) 공격 시나리오:** + +공격자는 공격 컨트랙트를 만들어 피싱 사이트를 통해 피공격 컨트랙트의 주인에게 공격 컨트랙트를 호출하도록 유도한다. tx.origin 은 최초 실행자이므로 피공격 컨트랙트의 withdraw 가 실행되게 된다. + +**3) 올바른 수정 방법:** +```solidity +// GOOD CODE - 수정된 코드를 작성하세요 +``` +require(msg.sender == owner, "Not owner"); +--- + +## 문제 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 +// 빈칸을 채운 완성 코드를 작성하세요 +// TODO: OpenZeppelin import +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +// TODO: 상속 추가 +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 - 언제 무엇을 사용하나요:** + +첫번째 방법은 직접 코드로 순서를 지켜야하는 번거로움이 있지만 두번째 방법은 라이브러리를 사용하므로 실수를 줄일 수 있다. +첫번째 방법은 단순하지만 두번째 방법은 복잡하다. + +--- + +## 문제 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 체크가 통과하는 이유는 무엇인가요? + +**답변:** + +밸런스 차감이 이루어지지 않아서 + +**질문 2:** CEI 패턴을 적용하면 6번에서 어떻게 되나요? + +**답변:** + +6번에서 남은 밸런스가 없어 require 체크가 통과하지 못한다. + +**질문 3:** 공격자가 총 몇 ETH를 탈취할 수 있나요? (예치금 1 ETH, Vault 총 잔액 10 ETH 가정) + +**답변:** + +10eth + +--- + +## 자기 평가 + +모든 문제를 풀었다면, 아래 체크리스트로 자기 평가를 해보세요: + +- [o] EVM의 결정론적 실행 필요성을 이해했다 +- [o] Storage/Memory/Stack의 차이와 비용을 알고 있다 +- [o] 재진입 공격의 원리를 설명할 수 있다 +- [o] CEI 패턴으로 재진입 공격을 방어할 수 있다 +- [o] tx.origin vs msg.sender의 보안 차이를 알고 있다 +- [o] 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`