diff --git a/.gitignore b/.gitignore
index ff3bed2..a250eee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,9 @@ node_modules/
# npm
package-lock.json
+
+# Vite
+dist
+*.local
+
+lib
\ No newline at end of file
diff --git a/week-04/dev/my-dapp/.env.example b/week-04/dev/my-dapp/.env.example
new file mode 100644
index 0000000..60be164
--- /dev/null
+++ b/week-04/dev/my-dapp/.env.example
@@ -0,0 +1,6 @@
+# Counter 컨트랙트 배포 주소
+# Anvil에서 배포 후 주소를 .env.local에 설정하세요
+VITE_COUNTER_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3
+
+# WalletConnect Project ID (https://cloud.walletconnect.com 에서 발급)
+VITE_WALLETCONNECT_PROJECT_ID=YOUR_PROJECT_ID
diff --git a/week-04/dev/my-dapp/assets/1-init.png b/week-04/dev/my-dapp/assets/1-init.png
new file mode 100644
index 0000000..bf64e86
Binary files /dev/null and b/week-04/dev/my-dapp/assets/1-init.png differ
diff --git a/week-04/dev/my-dapp/assets/2-connect.png b/week-04/dev/my-dapp/assets/2-connect.png
new file mode 100644
index 0000000..fad7be7
Binary files /dev/null and b/week-04/dev/my-dapp/assets/2-connect.png differ
diff --git a/week-04/dev/my-dapp/assets/3-increment.png b/week-04/dev/my-dapp/assets/3-increment.png
new file mode 100644
index 0000000..c63ab3f
Binary files /dev/null and b/week-04/dev/my-dapp/assets/3-increment.png differ
diff --git a/week-04/dev/my-dapp/assets/4-decrement.png b/week-04/dev/my-dapp/assets/4-decrement.png
new file mode 100644
index 0000000..8dff536
Binary files /dev/null and b/week-04/dev/my-dapp/assets/4-decrement.png differ
diff --git a/week-04/dev/my-dapp/index.html b/week-04/dev/my-dapp/index.html
new file mode 100644
index 0000000..06d42d0
--- /dev/null
+++ b/week-04/dev/my-dapp/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Counter DApp
+
+
+
+
+
+
diff --git a/week-04/dev/my-dapp/package.json b/week-04/dev/my-dapp/package.json
new file mode 100644
index 0000000..aad41da
--- /dev/null
+++ b/week-04/dev/my-dapp/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "counter-dapp",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@rainbow-me/rainbowkit": "^2.2.10",
+ "@tanstack/react-query": "^5.62.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "viem": "^2.21.0",
+ "wagmi": "^2.14.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.4",
+ "typescript": "~5.6.2",
+ "vite": "^6.0.0"
+ }
+}
diff --git a/week-04/dev/my-dapp/src/App.css b/week-04/dev/my-dapp/src/App.css
new file mode 100644
index 0000000..daa989b
--- /dev/null
+++ b/week-04/dev/my-dapp/src/App.css
@@ -0,0 +1,153 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: #1a1a2e;
+ color: #e0e0e0;
+ min-height: 100vh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.container {
+ max-width: 480px;
+ width: 100%;
+ padding: 2rem;
+ text-align: center;
+}
+
+h1 {
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ color: #fff;
+}
+
+.card {
+ background: #16213e;
+ border-radius: 12px;
+ padding: 1.5rem;
+ margin-bottom: 1rem;
+}
+
+.wallet-info {
+ font-size: 0.875rem;
+ color: #a0a0a0;
+ word-break: break-all;
+}
+
+.wallet-info .address {
+ color: #64b5f6;
+ font-family: monospace;
+}
+
+.wallet-info .chain {
+ color: #81c784;
+ margin-top: 0.25rem;
+}
+
+.count {
+ font-size: 4rem;
+ font-weight: bold;
+ color: #fff;
+ margin: 0.5rem 0;
+}
+
+.actions {
+ display: flex;
+ gap: 0.75rem;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+button {
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: opacity 0.2s, transform 0.1s;
+}
+
+button:hover:not(:disabled) {
+ opacity: 0.85;
+}
+
+button:active:not(:disabled) {
+ transform: scale(0.97);
+}
+
+button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.btn-connect {
+ background: #4361ee;
+ color: #fff;
+ width: 100%;
+}
+
+.btn-disconnect {
+ background: #e63946;
+ color: #fff;
+ width: 100%;
+ margin-top: 0.75rem;
+}
+
+.btn-increment {
+ background: #2ec4b6;
+ color: #fff;
+}
+
+.btn-decrement {
+ background: #ff6b6b;
+ color: #fff;
+}
+
+.btn-reset {
+ background: #6c757d;
+ color: #fff;
+}
+
+.tx-status {
+ margin-top: 0.75rem;
+ padding: 0.75rem;
+ border-radius: 8px;
+ font-size: 0.875rem;
+}
+
+.tx-status.pending {
+ background: #fff3cd;
+ color: #856404;
+}
+
+.tx-status.confirming {
+ background: #cce5ff;
+ color: #004085;
+}
+
+.tx-status.success {
+ background: #d4edda;
+ color: #155724;
+}
+
+.tx-status.error {
+ background: #f8d7da;
+ color: #721c24;
+}
+
+.loading {
+ color: #a0a0a0;
+ font-style: italic;
+}
+
+.error-text {
+ color: #e63946;
+ font-size: 0.875rem;
+}
diff --git a/week-04/dev/my-dapp/src/App.tsx b/week-04/dev/my-dapp/src/App.tsx
new file mode 100644
index 0000000..5ad6d70
--- /dev/null
+++ b/week-04/dev/my-dapp/src/App.tsx
@@ -0,0 +1,30 @@
+import '@rainbow-me/rainbowkit/styles.css'
+
+import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
+import { WagmiProvider } from 'wagmi'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { ConnectButton } from '@rainbow-me/rainbowkit'
+import { config } from './wagmi'
+import { CounterDisplay } from './components/CounterDisplay'
+import { CounterActions } from './components/CounterActions'
+
+const queryClient = new QueryClient()
+
+function App() {
+ return (
+
+
+
+
+
Counter DApp
+
+
+
+
+
+
+
+ )
+}
+
+export default App
diff --git a/week-04/dev/my-dapp/src/components/CounterActions.tsx b/week-04/dev/my-dapp/src/components/CounterActions.tsx
new file mode 100644
index 0000000..376ac7b
--- /dev/null
+++ b/week-04/dev/my-dapp/src/components/CounterActions.tsx
@@ -0,0 +1,59 @@
+import { useAccount, useWriteContract } from 'wagmi'
+import { useQueryClient } from '@tanstack/react-query'
+import { counterAbi, counterAddress } from '../counter'
+import { TransactionStatus } from './TransactionStatus'
+
+export function CounterActions() {
+ const { isConnected } = useAccount()
+ const queryClient = useQueryClient()
+ const { writeContract, data: hash, isPending, error, reset } = useWriteContract()
+
+ const handleWrite = (functionName: 'increment' | 'decrement' | 'reset') => {
+ reset()
+ writeContract({
+ address: counterAddress,
+ abi: counterAbi,
+ functionName,
+ })
+ }
+
+ const handleConfirmed = () => {
+ queryClient.invalidateQueries({ queryKey: ['readContract'] })
+ }
+
+ if (!isConnected) return null
+
+ return (
+
+
+ handleWrite('decrement')}
+ >
+ - Decrement
+
+ handleWrite('increment')}
+ >
+ + Increment
+
+ handleWrite('reset')}
+ >
+ Reset
+
+
+
+
+ )
+}
diff --git a/week-04/dev/my-dapp/src/components/CounterDisplay.tsx b/week-04/dev/my-dapp/src/components/CounterDisplay.tsx
new file mode 100644
index 0000000..ae35745
--- /dev/null
+++ b/week-04/dev/my-dapp/src/components/CounterDisplay.tsx
@@ -0,0 +1,42 @@
+import { useReadContract, useAccount } from 'wagmi'
+import { counterAbi, counterAddress } from '../counter'
+
+export function CounterDisplay() {
+ const { isConnected } = useAccount()
+ const { data, isLoading, isError, error } = useReadContract({
+ address: counterAddress,
+ abi: counterAbi,
+ functionName: 'getCount',
+ query: { enabled: isConnected },
+ })
+
+ if (!isConnected) {
+ return (
+
+ )
+ }
+
+ if (isLoading) {
+ return (
+
+ )
+ }
+
+ if (isError) {
+ return (
+
+
Error: {error?.message ?? 'Failed to read count'}
+
+ )
+ }
+
+ return (
+
+
{data?.toString() ?? '—'}
+
+ )
+}
diff --git a/week-04/dev/my-dapp/src/components/TransactionStatus.tsx b/week-04/dev/my-dapp/src/components/TransactionStatus.tsx
new file mode 100644
index 0000000..09acb98
--- /dev/null
+++ b/week-04/dev/my-dapp/src/components/TransactionStatus.tsx
@@ -0,0 +1,60 @@
+import { useEffect } from 'react'
+import { useWaitForTransactionReceipt } from 'wagmi'
+
+interface TransactionStatusProps {
+ hash: `0x${string}` | undefined
+ isPending: boolean
+ error: Error | null
+ onConfirmed?: () => void
+}
+
+export function TransactionStatus({ hash, isPending, error, onConfirmed }: TransactionStatusProps) {
+ const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
+ hash,
+ query: {
+ enabled: !!hash,
+ },
+ })
+
+ useEffect(() => {
+ if (isSuccess && onConfirmed) {
+ onConfirmed()
+ }
+ }, [isSuccess, onConfirmed])
+
+ if (!isPending && !hash && !error) return null
+
+ if (error) {
+ return (
+
+ Error: {error.message.split('\n')[0]}
+
+ )
+ }
+
+ if (isPending) {
+ return (
+
+ 지갑에서 트랜잭션을 확인해주세요...
+
+ )
+ }
+
+ if (isConfirming) {
+ return (
+
+ 트랜잭션 확인 중...
+
+ )
+ }
+
+ if (isSuccess) {
+ return (
+
+ 트랜잭션 성공!
+
+ )
+ }
+
+ return null
+}
diff --git a/week-04/dev/my-dapp/src/counter.ts b/week-04/dev/my-dapp/src/counter.ts
new file mode 100644
index 0000000..8c56d1f
--- /dev/null
+++ b/week-04/dev/my-dapp/src/counter.ts
@@ -0,0 +1,39 @@
+export const counterAbi = [
+ {
+ type: 'function',
+ name: 'count',
+ inputs: [],
+ outputs: [{ name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'getCount',
+ inputs: [],
+ outputs: [{ name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'increment',
+ inputs: [],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'decrement',
+ inputs: [],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'reset',
+ inputs: [],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+] as const
+
+export const counterAddress = import.meta.env.VITE_COUNTER_ADDRESS as `0x${string}`
diff --git a/week-04/dev/my-dapp/src/main.tsx b/week-04/dev/my-dapp/src/main.tsx
new file mode 100644
index 0000000..43528b2
--- /dev/null
+++ b/week-04/dev/my-dapp/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import App from './App'
+import './App.css'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/week-04/dev/my-dapp/src/vite-env.d.ts b/week-04/dev/my-dapp/src/vite-env.d.ts
new file mode 100644
index 0000000..c6c1093
--- /dev/null
+++ b/week-04/dev/my-dapp/src/vite-env.d.ts
@@ -0,0 +1,10 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_COUNTER_ADDRESS: string
+ readonly VITE_WALLETCONNECT_PROJECT_ID: string
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv
+}
diff --git a/week-04/dev/my-dapp/src/wagmi.ts b/week-04/dev/my-dapp/src/wagmi.ts
new file mode 100644
index 0000000..cd783b1
--- /dev/null
+++ b/week-04/dev/my-dapp/src/wagmi.ts
@@ -0,0 +1,13 @@
+import { getDefaultConfig } from '@rainbow-me/rainbowkit'
+import { http } from 'wagmi'
+import { foundry, sepolia } from 'wagmi/chains'
+
+export const config = getDefaultConfig({
+ appName: 'Counter DApp',
+ projectId: import.meta.env.VITE_WALLETCONNECT_PROJECT_ID,
+ chains: [foundry, sepolia],
+ transports: {
+ [foundry.id]: http(),
+ [sepolia.id]: http(),
+ },
+})
diff --git a/week-04/dev/my-dapp/tsconfig.json b/week-04/dev/my-dapp/tsconfig.json
new file mode 100644
index 0000000..02bc281
--- /dev/null
+++ b/week-04/dev/my-dapp/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/week-04/dev/my-dapp/tsconfig.node.json b/week-04/dev/my-dapp/tsconfig.node.json
new file mode 100644
index 0000000..be1e141
--- /dev/null
+++ b/week-04/dev/my-dapp/tsconfig.node.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "composite": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": false,
+ "declaration": true,
+ "declarationMap": true,
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/week-04/dev/my-dapp/vite.config.ts b/week-04/dev/my-dapp/vite.config.ts
new file mode 100644
index 0000000..9ffcc67
--- /dev/null
+++ b/week-04/dev/my-dapp/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+})
diff --git a/week-04/dev/script/Counter.s.sol b/week-04/dev/script/Counter.s.sol
new file mode 100644
index 0000000..8bb8fd4
--- /dev/null
+++ b/week-04/dev/script/Counter.s.sol
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.26;
+
+import "forge-std/Script.sol";
+import "../src/Counter.sol";
+
+contract CounterScript is Script {
+ function run() external {
+ vm.broadcast();
+ Counter counter = new Counter();
+ console.log("Counter deployed at:", address(counter));
+ }
+}
diff --git a/week-04/dev/src/Counter.sol b/week-04/dev/src/Counter.sol
new file mode 100644
index 0000000..f7cde30
--- /dev/null
+++ b/week-04/dev/src/Counter.sol
@@ -0,0 +1,23 @@
+// 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;
+ }
+
+ function reset() public {
+ count = 0;
+ }
+}
diff --git a/week-04/quiz/quiz-04-solution.md b/week-04/quiz/quiz-04-solution.md
new file mode 100644
index 0000000..55363f7
--- /dev/null
+++ b/week-04/quiz/quiz-04-solution.md
@@ -0,0 +1,392 @@
+# Week 4 Quiz: Network/Block + wagmi
+
+> **제출 방법:** 이 파일을 복사하여 답변을 작성한 후, PR로 제출하세요.
+> **평가 기준:** 개념 이해도 중심 - 문법 오류보다 논리적 설명을 중시합니다.
+
+---
+
+## 문제 1: 블록 헤더 필드 (객관식)
+
+다음 상황을 고려하세요:
+
+```
+블록 100의 해시: 0xabc123...
+블록 101의 해시: 0xdef456...
+```
+
+블록 101의 `parentHash` 필드에는 어떤 값이 저장되어 있나요? 그리고 **왜** 이런 방식으로 연결하나요?
+
+**보기:**
+A) 0xdef456... - 자기 자신의 해시를 저장하여 무결성을 보장한다
+B) 0xabc123... - 이전 블록의 해시를 저장하여 체인 연결과 불변성을 보장한다
+C) 블록 번호 100 - 숫자로 순서를 추적한다
+D) 빈 값 - 헤더에는 해시가 저장되지 않는다
+
+**답변:**
+
+B, 다음 블록은 부모 블록의 해시를 가지고 있어야한다. 말그대로 체인이기때문이다. A 값이 바뀌면 B 값도 바뀌어야한다.
+
+A, 역설이다. 해시는 블록이 생성된 이후에 계산된다. 생성되기도 전에 해시를 알 수 없다.
+C, 해시는 완전 무결한 값이기에 블록 번호로 하면 안된다. 뭐가 바뀌었는지 모른다.
+D, 헤더에 아무것도 안하면 블록체인이 아니다.
+
+---
+
+## 문제 2: MPT 목적 (객관식)
+
+이더리움에서 Merkle Patricia Trie(MPT)를 사용하는 **가장 중요한 이유**는 무엇인가요?
+
+**보기:**
+A) 데이터를 암호화하여 외부에서 읽을 수 없게 한다
+B) 트랜잭션 처리 속도를 10배 이상 높인다
+C) 전체 데이터 없이도 특정 데이터의 존재와 정확성을 효율적으로 증명한다
+D) 블록 크기를 줄여서 저장 공간을 절약한다
+
+**답변:**
+
+C, 머클 증명을 통해 데이터 처리에 한계가 있는 디바이스가 쉽게 데이터 확인을 할 수 있다.
+
+---
+
+## 문제 3: 체인 연결과 보안 (객관식)
+
+공격자가 블록 50의 트랜잭션을 수정하려고 합니다. 현재 체인의 최신 블록은 100입니다. 이 공격이 **왜** 어려운가요?
+
+**보기:**
+A) 블록 50은 너무 오래되어서 시스템에서 접근할 수 없다
+B) 블록 50을 수정하면 해시가 바뀌고, 블록 51부터 100까지 모든 블록의 parentHash가 불일치하게 된다
+C) 블록 50은 이미 암호화되어 있어서 복호화 키가 필요하다
+D) 네트워크 관리자만 과거 블록을 수정할 수 있다
+
+**답변:**
+
+블록 100 부터 50까지 전부 바꿔야하므로 매우 어려운 작업이다. 근데 101, 102 도 계속 생성되니 바꾸기가 비현실적인것.
+
+---
+
+## 문제 4: MPT 진화 과정 (단답형)
+
+MPT(Merkle Patricia Trie)는 세 가지 자료구조의 장점을 결합한 것입니다:
+1. **Trie** -> 2. **Patricia Trie** -> 3. **Merkle Patricia Trie**
+
+**왜** 각 단계의 발전이 필요했나요? 각 단계가 해결하는 문제를 간단히 설명하세요.
+
+**답변:**
+
+1. Trie가 해결하는 문제:
+- 사전구조와 비슷하므로 데이터 길이만 길어질뿐 찾는 속도는 빠르다. 하지만 데이터가 길어지면 메모리 낭비가 심하다.
+2. Patricia Trie가 해결하는 문제 (Trie의 한계):
+- 자식 노드가 하나뿐인 노드를 합쳐버렸다. 저장공간을 줄였다. 하지만 수정이 되었는지 증명하는 방법이 부족했다.
+3. Merkle Patricia Trie가 해결하는 문제 (Patricia Trie의 한계):
+- 머클 증명을 통해 트리 전체를 대표하는 머클 해시를 만들어 굳이 전체 탐색을 안해도 증명할 수 있게 되었다.
+
+
+
+---
+
+## 문제 5: Eclipse Attack 방어 (단답형)
+
+Eclipse Attack은 공격자가 피해자 노드의 **모든 피어 연결**을 자신이 통제하는 노드로 바꾸는 공격입니다.
+
+1) 이 공격이 성공하면 피해자에게 **어떤 피해**가 발생할 수 있나요?
+2) 개인 노드 운영자가 이 공격을 **방어**하기 위해 할 수 있는 행동은 무엇인가요?
+
+**답변:**
+1) 가능한 피해 (2가지 이상):
+- 이중지불
+- 채굴 낭비
+- 담합
+
+2) 방어 방법 (2가지 이상):
+- 신뢰할만한 노드 추가
+- 피어 교체
+- ip 다양성 확보
+
+
+---
+
+## 문제 6: 노드 종류 선택 (단답형)
+
+친구가 이더리움 개발을 시작하려고 합니다. 다음 세 가지 상황에서 각각 어떤 노드 타입(Full, Light, Archive)을 추천하시겠습니까? **왜** 그 노드를 추천하는지도 설명하세요.
+
+1) 모바일 지갑 앱 개발
+2) 블록체인 데이터 분석 서비스 개발
+3) 일반적인 dApp 백엔드 개발
+
+**답변:**
+1) 모바일 지갑 앱:
+ 추천 노드: Light Node
+ 이유: 모바일 환경은 제한된 저장공간을 가지고있다. Light Node는 모든 블록 데이터를 받지 않고 블록 헤더 구조만 다운로드하여 용량을 획기적으로 줄이면서도, 머클 증명을 통해 트랜잭션의 유효성을 검증할 수 있기 때문에 모바일 환경에 적합하다.
+
+2) 블록체인 데이터 분석:
+ 추천 노드: Archive Node
+ 이유: Archive Node는 블록체인의 모든 기록과 과거의 상태(State)를 빠짐없이 가지고 있어 데이터 조회 요구사항을 충족할 수 있다.
+
+3) dApp 백엔드:
+ 추천 노드: Full Node
+ 이유: 일반적인 dApp 운영에서는 새 트랜잭션을 검증 및 전파하고, 최신 블록체인 상태 데이터를 읽어오는 것이 가장 중요하다. Full Node는 이러한 독자적인 데이터 검증 및 상태 관리 등을 자체적으로 처리할 수 있으면서도, Archive Node에 비해 스토리지 용량 유지 및 운영 비용 면에서 훨씬 합리적이다.
+
+---
+
+## 문제 7: useAccount Hook (빈칸 채우기)
+
+다음 코드의 빈칸을 채워서 지갑 연결 상태를 표시하는 컴포넌트를 완성하세요:
+
+```typescript
+import { _________________ } from 'wagmi';
+
+function WalletStatus() {
+ // TODO: useAccount hook에서 필요한 값들을 가져오세요
+ const { _________________, _________________ } = useAccount();
+
+ if (!isConnected) {
+ return 지갑이 연결되지 않았습니다
;
+ }
+
+ return (
+
+ );
+}
+```
+
+**답변:**
+```typescript
+import { useAccount } from 'wagmi';
+
+function WalletStatus() {
+ // TODO: useAccount hook에서 필요한 값들을 가져오세요
+ const { isConnected, address } = useAccount();
+
+ if (!isConnected) {
+ return 지갑이 연결되지 않았습니다
;
+ }
+
+ return (
+
+ );
+}
+```
+
+**왜 이렇게 작성했나요:**
+
+
+useAccount 훅은 현재 지갑 프로바이더에 연결이 되어있는지 여부를 판단하는 isConnected와 연결된 지갑의 주소를 반환하는 address를 제공한다.
+
+**3버전 부터는 useAccount -> useConnection 으로 변경되었습니다~ 참고하시길 바랍니다.**
+---
+
+## 문제 8: useReadContract Hook (빈칸 채우기)
+
+다음 코드의 빈칸을 채워서 컨트랙트의 `getCount` 함수 결과를 화면에 표시하세요:
+
+```typescript
+import { useReadContract } from 'wagmi';
+
+const counterABI = [
+ {
+ name: 'getCount',
+ type: 'function',
+ stateMutability: 'view',
+ inputs: [],
+ outputs: [{ name: 'count', type: 'uint256' }],
+ },
+] as const;
+
+function CountDisplay() {
+ const { data, isLoading, error } = useReadContract({
+ // TODO: 필요한 설정을 채우세요
+ address: '0x1234...5678',
+ _________________,
+ _________________,
+ });
+
+ if (isLoading) return 로딩 중...
;
+ if (error) return 에러 발생
;
+
+ return 현재 카운트: {_________________}
;
+}
+```
+
+**답변:**
+```typescript
+// 완성된 코드를 여기에 작성하세요
+function CountDisplay() {
+ const { data, isLoading, error } = useReadContract({
+ // TODO: 필요한 설정을 채우세요
+ address: '0x1234...5678',
+ abi: counterABI,
+ functionName: 'getCount',
+ });
+
+ if (isLoading) return 로딩 중...
;
+ if (error) return 에러 발생
;
+
+ return 현재 카운트: {data}
;
+}
+```
+
+**왜 이렇게 작성했나요:**
+
+useReadContract 함수는 컨트랙트 주소, abi, 함수 이름이 필요합니다. data 는 함수 로딩이 끝난후에만 확인가능합니다. 에러를 안받으려면 위에서 조건처리를 해야합니다.
+
+---
+
+## 문제 9: useWriteContract 버그 (취약점 찾기)
+
+다음 코드에서 **문제점**을 찾고 수정하세요:
+
+```typescript
+// BAD CODE - 문제점 찾기
+import { useWriteContract } from 'wagmi';
+
+function IncrementButton() {
+ const { writeContract, isPending } = useWriteContract();
+
+ const handleClick = () => {
+ // 문제가 있는 코드
+ writeContract({
+ address: '0x1234...5678',
+ functionName: 'increment',
+ // abi가 없음!
+ });
+ };
+
+ return (
+
+ 증가하기
+
+ );
+}
+```
+
+**1) 발견한 문제점:**
+
+abi 가 없습니다.
+
+**2) 왜 이것이 문제인가:**
+
+abi 를 통해 타입검증을 해야하는데 못해서 에러를 뿜습니다.
+
+**3) 올바른 수정 방법:**
+```typescript
+// GOOD CODE - 수정된 버전을 작성하세요
+import { useWriteContract } from 'wagmi';
+
+function IncrementButton() {
+ const { writeContract, isPending } = useWriteContract();
+
+ const handleClick = () => {
+ // 문제가 있는 코드
+ writeContract({
+ address: '0x1234...5678',
+ functionName: 'increment',
+ abi: counterABI,
+ });
+ };
+
+ return (
+
+ 증가하기
+
+ );
+}
+```
+
+**3버전 부터는 약간 개발자 편의성이 증대했습니다. 이제 못생기게 writeTransaction, isLoading .. 하지말고 하나의 객체로 받아오면 됩니다.**
+
+---
+
+## 문제 10: 블록 연결 구조 (다이어그램 해석)
+
+다음 다이어그램은 블록체인의 연결 구조를 보여줍니다:
+
+```mermaid
+graph LR
+ subgraph B0["제네시스 블록"]
+ H0["hash: 0xabc..."]
+ end
+ subgraph B1["블록 1"]
+ PH1["parent: 0xabc..."]
+ H1["hash: 0xdef..."]
+ end
+ subgraph B2["블록 2"]
+ PH2["parent: 0xdef..."]
+ H2["hash: 0x123..."]
+ end
+ subgraph B3["블록 3"]
+ PH3["parent: ???"]
+ H3["hash: 0x789..."]
+ end
+
+ B0 --> B1 --> B2 --> B3
+```
+
+**질문:**
+
+1) 블록 3의 `parent: ???` 에 들어갈 값은 무엇인가요?
+- 0x123...
+
+2) 만약 블록 1의 내용이 수정되면, 블록 2와 블록 3에 **어떤 영향**이 있나요? 왜 그런가요?
+- 해시값이 수정된다. 부모 해시의 값이 해시 생성에 포함되기 때문이다.
+
+3) 제네시스 블록(블록 0)의 parentHash는 어떤 특별한 값을 가지나요? 왜 그런가요?
+- 0x000... 최초 블록이기에 0 이다. 비트코인은 여기에 유명한 글을 쓰기도 했다.
+
+---
+
+## 문제 11: MPT 트리 구조 (다이어그램 해석)
+
+다음 다이어그램은 MPT의 노드 구조를 보여줍니다:
+
+```mermaid
+graph TD
+ ROOT["Root Hash: 0xfff..."] --> EXT1["Extension Node path: 0a"]
+ ROOT --> EXT2["Extension Node path: 0b"]
+
+ EXT1 --> BRANCH["Branch Node (16개 슬롯)"]
+ BRANCH --> LEAF1["Leaf: 계정 A 주소: 0a1234..."]
+ BRANCH --> LEAF2["Leaf: 계정 B 주소: 0a5678..."]
+
+ EXT2 --> LEAF3["Leaf: 계정 C 주소: 0b9999..."]
+```
+
+**질문:**
+
+1) 계정 A와 계정 B가 같은 Branch Node 아래에 있는 이유는 무엇인가요? (주소 패턴을 힌트로 사용하세요)
+- 공통 접두어로 시작하기 때문에 (0a)
+
+2) Extension Node가 하는 역할은 무엇인가요? 없다면 어떤 문제가 생기나요?
+- 자식이 하나뿐인 연속된 경로를 하나로 합침. 글자마다 노드를 생성하기 떄문에 용량이 커진다.
+
+3) Root Hash만 알면 어떻게 특정 계정의 데이터 존재를 **증명**할 수 있나요? (Light Client 관점에서)
+- 머클 증명을 사용하기 떄문에 데이터 변경시 머클 해시가 변경된다. 라이트 클라이언트는 전체 노드를 다 몰라도 되니까 머클 해시만 알면 된다.
+
+---
+
+## 제출 전 체크리스트
+
+- [ ] 모든 문제에 답변을 작성했는가?
+- [ ] 객관식 문제: 정답 선택 **이유**를 설명했는가?
+- [ ] 단답형 문제: 2-3문장 이상으로 충분히 설명했는가?
+- [ ] 코드 문제: 완성된 코드와 **왜 그렇게 작성했는지** 설명했는가?
+- [ ] 다이어그램 문제: 각 질문에 논리적으로 답변했는가?
diff --git a/week-04/quiz/quiz-04.md b/week-04/quiz/quiz-04.md
index 91dfa07..16f4f3c 100644
--- a/week-04/quiz/quiz-04.md
+++ b/week-04/quiz/quiz-04.md
@@ -116,20 +116,6 @@ Eclipse Attack은 공격자가 피해자 노드의 **모든 피어 연결**을
3) 일반적인 dApp 백엔드 개발
**답변:**
-
-
---
diff --git a/week-05/dev/my-dapp/.env.example b/week-05/dev/my-dapp/.env.example
new file mode 100644
index 0000000..60be164
--- /dev/null
+++ b/week-05/dev/my-dapp/.env.example
@@ -0,0 +1,6 @@
+# Counter 컨트랙트 배포 주소
+# Anvil에서 배포 후 주소를 .env.local에 설정하세요
+VITE_COUNTER_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3
+
+# WalletConnect Project ID (https://cloud.walletconnect.com 에서 발급)
+VITE_WALLETCONNECT_PROJECT_ID=YOUR_PROJECT_ID
diff --git a/week-05/dev/my-dapp/index.html b/week-05/dev/my-dapp/index.html
new file mode 100644
index 0000000..06d42d0
--- /dev/null
+++ b/week-05/dev/my-dapp/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Counter DApp
+
+
+
+
+
+
diff --git a/week-05/dev/my-dapp/package.json b/week-05/dev/my-dapp/package.json
new file mode 100644
index 0000000..aad41da
--- /dev/null
+++ b/week-05/dev/my-dapp/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "counter-dapp",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@rainbow-me/rainbowkit": "^2.2.10",
+ "@tanstack/react-query": "^5.62.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "viem": "^2.21.0",
+ "wagmi": "^2.14.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.4",
+ "typescript": "~5.6.2",
+ "vite": "^6.0.0"
+ }
+}
diff --git a/week-05/dev/my-dapp/src/App.css b/week-05/dev/my-dapp/src/App.css
new file mode 100644
index 0000000..daa989b
--- /dev/null
+++ b/week-05/dev/my-dapp/src/App.css
@@ -0,0 +1,153 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: #1a1a2e;
+ color: #e0e0e0;
+ min-height: 100vh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.container {
+ max-width: 480px;
+ width: 100%;
+ padding: 2rem;
+ text-align: center;
+}
+
+h1 {
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ color: #fff;
+}
+
+.card {
+ background: #16213e;
+ border-radius: 12px;
+ padding: 1.5rem;
+ margin-bottom: 1rem;
+}
+
+.wallet-info {
+ font-size: 0.875rem;
+ color: #a0a0a0;
+ word-break: break-all;
+}
+
+.wallet-info .address {
+ color: #64b5f6;
+ font-family: monospace;
+}
+
+.wallet-info .chain {
+ color: #81c784;
+ margin-top: 0.25rem;
+}
+
+.count {
+ font-size: 4rem;
+ font-weight: bold;
+ color: #fff;
+ margin: 0.5rem 0;
+}
+
+.actions {
+ display: flex;
+ gap: 0.75rem;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+button {
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: opacity 0.2s, transform 0.1s;
+}
+
+button:hover:not(:disabled) {
+ opacity: 0.85;
+}
+
+button:active:not(:disabled) {
+ transform: scale(0.97);
+}
+
+button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.btn-connect {
+ background: #4361ee;
+ color: #fff;
+ width: 100%;
+}
+
+.btn-disconnect {
+ background: #e63946;
+ color: #fff;
+ width: 100%;
+ margin-top: 0.75rem;
+}
+
+.btn-increment {
+ background: #2ec4b6;
+ color: #fff;
+}
+
+.btn-decrement {
+ background: #ff6b6b;
+ color: #fff;
+}
+
+.btn-reset {
+ background: #6c757d;
+ color: #fff;
+}
+
+.tx-status {
+ margin-top: 0.75rem;
+ padding: 0.75rem;
+ border-radius: 8px;
+ font-size: 0.875rem;
+}
+
+.tx-status.pending {
+ background: #fff3cd;
+ color: #856404;
+}
+
+.tx-status.confirming {
+ background: #cce5ff;
+ color: #004085;
+}
+
+.tx-status.success {
+ background: #d4edda;
+ color: #155724;
+}
+
+.tx-status.error {
+ background: #f8d7da;
+ color: #721c24;
+}
+
+.loading {
+ color: #a0a0a0;
+ font-style: italic;
+}
+
+.error-text {
+ color: #e63946;
+ font-size: 0.875rem;
+}
diff --git a/week-05/dev/my-dapp/src/App.tsx b/week-05/dev/my-dapp/src/App.tsx
new file mode 100644
index 0000000..7c5754a
--- /dev/null
+++ b/week-05/dev/my-dapp/src/App.tsx
@@ -0,0 +1,30 @@
+import '@rainbow-me/rainbowkit/styles.css'
+
+import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
+import { WagmiProvider } from 'wagmi'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { config } from './wagmi'
+import { CounterDisplay } from './components/CounterDisplay'
+import { CounterActions } from './components/CounterActions'
+import CustomWalletButton from './components/CustomWalletButton'
+import SenEthDisplay from './components/SenEthDisplay'
+
+const queryClient = new QueryClient()
+
+function App() {
+ return (
+
+
+
+
+
Counter DApp
+
+
+
+
+
+
+ )
+}
+
+export default App
diff --git a/week-05/dev/my-dapp/src/components/CounterActions.tsx b/week-05/dev/my-dapp/src/components/CounterActions.tsx
new file mode 100644
index 0000000..376ac7b
--- /dev/null
+++ b/week-05/dev/my-dapp/src/components/CounterActions.tsx
@@ -0,0 +1,59 @@
+import { useAccount, useWriteContract } from 'wagmi'
+import { useQueryClient } from '@tanstack/react-query'
+import { counterAbi, counterAddress } from '../counter'
+import { TransactionStatus } from './TransactionStatus'
+
+export function CounterActions() {
+ const { isConnected } = useAccount()
+ const queryClient = useQueryClient()
+ const { writeContract, data: hash, isPending, error, reset } = useWriteContract()
+
+ const handleWrite = (functionName: 'increment' | 'decrement' | 'reset') => {
+ reset()
+ writeContract({
+ address: counterAddress,
+ abi: counterAbi,
+ functionName,
+ })
+ }
+
+ const handleConfirmed = () => {
+ queryClient.invalidateQueries({ queryKey: ['readContract'] })
+ }
+
+ if (!isConnected) return null
+
+ return (
+
+
+ handleWrite('decrement')}
+ >
+ - Decrement
+
+ handleWrite('increment')}
+ >
+ + Increment
+
+ handleWrite('reset')}
+ >
+ Reset
+
+
+
+
+ )
+}
diff --git a/week-05/dev/my-dapp/src/components/CounterDisplay.tsx b/week-05/dev/my-dapp/src/components/CounterDisplay.tsx
new file mode 100644
index 0000000..ae35745
--- /dev/null
+++ b/week-05/dev/my-dapp/src/components/CounterDisplay.tsx
@@ -0,0 +1,42 @@
+import { useReadContract, useAccount } from 'wagmi'
+import { counterAbi, counterAddress } from '../counter'
+
+export function CounterDisplay() {
+ const { isConnected } = useAccount()
+ const { data, isLoading, isError, error } = useReadContract({
+ address: counterAddress,
+ abi: counterAbi,
+ functionName: 'getCount',
+ query: { enabled: isConnected },
+ })
+
+ if (!isConnected) {
+ return (
+
+ )
+ }
+
+ if (isLoading) {
+ return (
+
+ )
+ }
+
+ if (isError) {
+ return (
+
+
Error: {error?.message ?? 'Failed to read count'}
+
+ )
+ }
+
+ return (
+
+
{data?.toString() ?? '—'}
+
+ )
+}
diff --git a/week-05/dev/my-dapp/src/components/CustomWalletButton.tsx b/week-05/dev/my-dapp/src/components/CustomWalletButton.tsx
new file mode 100644
index 0000000..6dc31e3
--- /dev/null
+++ b/week-05/dev/my-dapp/src/components/CustomWalletButton.tsx
@@ -0,0 +1,39 @@
+import { ConnectButton } from "@rainbow-me/rainbowkit";
+import { useAccount, useDisconnect } from "wagmi";
+
+export default function CustomWalletButton() {
+ const { isConnected, address, chainId } = useAccount();
+ const { disconnect } = useDisconnect();
+ return(
+
+ {({ account, chain, openAccountModal, openChainModal, openConnectModal, mounted }) => {
+ // 서버 사이드 렌더링과 클라이언트 렌더링을 맞추기 위해 mounted 체크 필요
+ if (!mounted) return null;
+
+ return (
+
+ {isConnected ? (
+
+ {/* 네트워크/체인 변경 모달 띄우기 */}
+
+ {chain?.name || chainId} 변경
+
+ {/* 계정 정보 모달 띄우기 */}
+
+ {account?.displayName || address}
+
+ disconnect()} type="button">
+ 연결 끊기
+
+
+ ) : (
+
+ 지갑 연결하기
+
+ )}
+
+ );
+ }}
+
+);
+}
diff --git a/week-05/dev/my-dapp/src/components/SenEthDisplay.tsx b/week-05/dev/my-dapp/src/components/SenEthDisplay.tsx
new file mode 100644
index 0000000..7199587
--- /dev/null
+++ b/week-05/dev/my-dapp/src/components/SenEthDisplay.tsx
@@ -0,0 +1,108 @@
+import React, { useState } from 'react';
+import { useAccount, useBalance, useSendTransaction, useWaitForTransactionReceipt } from 'wagmi';
+import { parseEther } from 'viem';
+
+export default function SenEthDisplay() {
+ const [toAddress, setToAddress] = useState('');
+ const [amount, setAmount] = useState('');
+
+ // 내 지갑 정보와 잔액 가져오기
+ const { address } = useAccount();
+ const { data: balanceData } = useBalance({ address });
+
+ // ETH 전송을 위한 훅
+ const {
+ data: hash,
+ isPending,
+ error: sendError,
+ sendTransaction
+ } = useSendTransaction();
+
+ // 트랜잭션이 블록에 포함될 때까지 기다리기 위한 훅
+ const { isLoading: isConfirming, isSuccess: isConfirmed } =
+ useWaitForTransactionReceipt({
+ hash,
+ });
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!toAddress || !amount) return;
+
+ // 이더를 Wei 단위로 변환 후 전송 (ex. 0.1 ETH -> 100000000000000000 Wei)
+ sendTransaction({
+ to: toAddress as `0x${string}`,
+ value: parseEther(amount),
+ });
+ };
+
+ return (
+
+
이더(ETH) 전송하기
+
+ {balanceData && (
+
+ 내 지갑 잔액: {Number(balanceData.formatted).toFixed(4)} {balanceData.symbol}
+
+ )}
+
+
+
+
+ {hash && (
+
+ 트랜잭션 해시: {hash.slice(0, 10)}...{hash.slice(-8)}
+
+ )}
+ {isConfirming &&
⏳ 트랜잭션이 블록에 포함되기를 기다리는 중입니다...
}
+ {isConfirmed &&
✅ 전송 완료! 트랜잭션이 성공적으로 처리되었습니다.
}
+ {sendError && (
+
+ ❌ 전송 실패: {((sendError as any).shortMessage || sendError.message)}
+
+ )}
+
+
+ );
+}
diff --git a/week-05/dev/my-dapp/src/components/TransactionStatus.tsx b/week-05/dev/my-dapp/src/components/TransactionStatus.tsx
new file mode 100644
index 0000000..09acb98
--- /dev/null
+++ b/week-05/dev/my-dapp/src/components/TransactionStatus.tsx
@@ -0,0 +1,60 @@
+import { useEffect } from 'react'
+import { useWaitForTransactionReceipt } from 'wagmi'
+
+interface TransactionStatusProps {
+ hash: `0x${string}` | undefined
+ isPending: boolean
+ error: Error | null
+ onConfirmed?: () => void
+}
+
+export function TransactionStatus({ hash, isPending, error, onConfirmed }: TransactionStatusProps) {
+ const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
+ hash,
+ query: {
+ enabled: !!hash,
+ },
+ })
+
+ useEffect(() => {
+ if (isSuccess && onConfirmed) {
+ onConfirmed()
+ }
+ }, [isSuccess, onConfirmed])
+
+ if (!isPending && !hash && !error) return null
+
+ if (error) {
+ return (
+
+ Error: {error.message.split('\n')[0]}
+
+ )
+ }
+
+ if (isPending) {
+ return (
+
+ 지갑에서 트랜잭션을 확인해주세요...
+
+ )
+ }
+
+ if (isConfirming) {
+ return (
+
+ 트랜잭션 확인 중...
+
+ )
+ }
+
+ if (isSuccess) {
+ return (
+
+ 트랜잭션 성공!
+
+ )
+ }
+
+ return null
+}
diff --git a/week-05/dev/my-dapp/src/counter.ts b/week-05/dev/my-dapp/src/counter.ts
new file mode 100644
index 0000000..8c56d1f
--- /dev/null
+++ b/week-05/dev/my-dapp/src/counter.ts
@@ -0,0 +1,39 @@
+export const counterAbi = [
+ {
+ type: 'function',
+ name: 'count',
+ inputs: [],
+ outputs: [{ name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'getCount',
+ inputs: [],
+ outputs: [{ name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'increment',
+ inputs: [],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'decrement',
+ inputs: [],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'reset',
+ inputs: [],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+] as const
+
+export const counterAddress = import.meta.env.VITE_COUNTER_ADDRESS as `0x${string}`
diff --git a/week-05/dev/my-dapp/src/main.tsx b/week-05/dev/my-dapp/src/main.tsx
new file mode 100644
index 0000000..43528b2
--- /dev/null
+++ b/week-05/dev/my-dapp/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import App from './App'
+import './App.css'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/week-05/dev/my-dapp/src/vite-env.d.ts b/week-05/dev/my-dapp/src/vite-env.d.ts
new file mode 100644
index 0000000..c6c1093
--- /dev/null
+++ b/week-05/dev/my-dapp/src/vite-env.d.ts
@@ -0,0 +1,10 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_COUNTER_ADDRESS: string
+ readonly VITE_WALLETCONNECT_PROJECT_ID: string
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv
+}
diff --git a/week-05/dev/my-dapp/src/wagmi.ts b/week-05/dev/my-dapp/src/wagmi.ts
new file mode 100644
index 0000000..cd783b1
--- /dev/null
+++ b/week-05/dev/my-dapp/src/wagmi.ts
@@ -0,0 +1,13 @@
+import { getDefaultConfig } from '@rainbow-me/rainbowkit'
+import { http } from 'wagmi'
+import { foundry, sepolia } from 'wagmi/chains'
+
+export const config = getDefaultConfig({
+ appName: 'Counter DApp',
+ projectId: import.meta.env.VITE_WALLETCONNECT_PROJECT_ID,
+ chains: [foundry, sepolia],
+ transports: {
+ [foundry.id]: http(),
+ [sepolia.id]: http(),
+ },
+})
diff --git a/week-05/dev/my-dapp/tsconfig.json b/week-05/dev/my-dapp/tsconfig.json
new file mode 100644
index 0000000..02bc281
--- /dev/null
+++ b/week-05/dev/my-dapp/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/week-05/dev/my-dapp/tsconfig.node.json b/week-05/dev/my-dapp/tsconfig.node.json
new file mode 100644
index 0000000..be1e141
--- /dev/null
+++ b/week-05/dev/my-dapp/tsconfig.node.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "composite": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": false,
+ "declaration": true,
+ "declarationMap": true,
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/week-05/dev/my-dapp/vite.config.ts b/week-05/dev/my-dapp/vite.config.ts
new file mode 100644
index 0000000..9ffcc67
--- /dev/null
+++ b/week-05/dev/my-dapp/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+})
diff --git a/week-05/dev/script/Counter.s.sol b/week-05/dev/script/Counter.s.sol
new file mode 100644
index 0000000..af4e481
--- /dev/null
+++ b/week-05/dev/script/Counter.s.sol
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.26;
+
+import "forge-std/Script.sol";
+import "../src/Counter.sol";
+
+contract CounterScript is Script {
+ function run() external {
+ vm.startBroadcast();
+
+ // 1. 카운터 컨트랙트 배포
+ Counter counter = new Counter();
+ console.log("Counter deployed at:", address(counter));
+
+ // 2. 특정 주소로 테스트 이더(ETH) 전송하기
+ address payable toUser = payable(0xF8D09e078D3552Ba1a5ae9876D3b24AA10B1EFAD);
+ uint256 amountToFund = 10 ether;
+
+ // 이더 전송 후 성공 여부 확인
+ (bool success, ) = toUser.call{ value: amountToFund }("");
+ require(success, "ETH Transfer failed in deployment script");
+
+ console.log("Sent", amountToFund / 1e18, "ETH to:", toUser);
+
+ vm.stopBroadcast();
+ }
+}
diff --git a/week-05/dev/src/Counter.sol b/week-05/dev/src/Counter.sol
new file mode 100644
index 0000000..f7cde30
--- /dev/null
+++ b/week-05/dev/src/Counter.sol
@@ -0,0 +1,23 @@
+// 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;
+ }
+
+ function reset() public {
+ count = 0;
+ }
+}
diff --git a/week-05/theory/quiz-05-solution.md b/week-05/theory/quiz-05-solution.md
new file mode 100644
index 0000000..ff4b9cc
--- /dev/null
+++ b/week-05/theory/quiz-05-solution.md
@@ -0,0 +1,425 @@
+# Week 5 Quiz: PoS/Consensus + RainbowKit
+
+> **제출 방법:** 이 파일을 복사하여 답변을 작성한 후, PR로 제출하세요.
+> **평가 기준:** 개념 이해도 중심 - 문법 오류보다 논리적 설명을 중시합니다.
+
+---
+
+## 문제 1: PoS 개념 (객관식)
+
+이더리움이 PoW(작업 증명)에서 PoS(지분 증명)로 전환한 **가장 주요한 이유**는 무엇인가요?
+
+**보기:**
+A) 트랜잭션 처리 속도를 10배 이상 높이기 위해
+B) 에너지 소비를 99.95% 이상 줄이고 환경 친화적으로 만들기 위해
+C) 블록 크기를 늘려서 더 많은 데이터를 저장하기 위해
+D) 채굴 장비 없이도 누구나 블록을 생성할 수 있게 하기 위해
+
+**답변:**
+B) PoW는 채굴 과정에서 대량의 전력을 소비하는 것이 가장 큰 문제였다. PoS는 연산 경쟁 대신 ETH 스테이킹으로 블록을 검증하므로 에너지 소비를 99.95% 이상 절감하여 환경 친화적으로 만든 것이 핵심 이유이다.
+
+
+---
+
+## 문제 2: 검증자 역할 (객관식)
+
+이더리움 PoS에서 검증자(Validator)가 수행하는 **두 가지 주요 역할**은 무엇인가요?
+
+**보기:**
+A) 블록 채굴(Mining)과 가스 가격 결정
+B) 블록 제안(Proposing)과 블록 증명(Attesting)
+C) 트랜잭션 전송과 수수료 수집
+D) 스마트 컨트랙트 배포와 실행
+
+**답변:**
+B) 블록 제안은 선택된 검증자가 새로운 블록을 생성하여 네트워크에 제안하는 역할이다. 블록 증명은 나머지 검증자들이 제안된 블록이 유효한지 확인하고 투표하는 역할이다.
+
+
+---
+
+## 문제 3: 왜 PoW에서 PoS로? (단답형)
+
+PoW(작업 증명)와 PoS(지분 증명)의 **핵심 차이점**은 무엇인가요?
+"자격 증명 방식"과 "보안 보장 방식" 두 관점에서 각각 비교하세요.
+
+**답변:**
+
+자격 증명 방식:
+- PoW: 해시 연산 경쟁에서 가장 먼저 정답을 찾은 채굴자가 블록 생성 자격을 얻는다.
+- PoS: 32 ETH를 스테이킹한 검증자 중 무작위로 선택된 자가 블록 제안 자격을 얻는다.
+
+보안 보장 방식:
+- PoW: 네트워크 공격을 위해 전체 해시파워의 51% 이상을 확보해야 하므로 막대한 하드웨어/전력 비용이 공격 억제력이 된다.
+- PoS: 공격을 시도하면 스테이킹한 ETH가 소각되므로 경제적 손실이 공격 억제력이 된다.
+
+
+---
+
+## 문제 4: 슬래싱의 목적 (단답형)
+
+슬래싱(Slashing)은 검증자의 스테이킹된 ETH를 **강제로 소각**하는 패널티입니다.
+
+1) 슬래싱이 발동되는 **두 가지 조건**은 무엇인가요?
+2) **왜** 이런 처벌이 필요한가요? 없다면 어떤 문제가 생길 수 있나요?
+
+**답변:**
+
+1) 슬래싱 조건 (2가지):
+ - 이중 투표: 같은 슬롯에서 서로 다른 두 블록에 동시에 투표하는 행위이다.
+ - 서라운드 투표: 이전 증명을 감싸는 모순된 증명을 제출하는 행위이다.
+
+2) 슬래싱이 필요한 이유:
+ 슬래싱이 없으면 검증자가 아무런 경제적 위험 없이 악의적 행동을 시도할 수 있다. 슬래싱은 부정 행위 시 스테이킹한 ETH를 잃게 함으로써 공격 비용을 극대화하고, 검증자가 정직하게 행동하도록 경제적 인센티브를 제공한다.
+
+
+---
+
+## 문제 5: 체인 선택 규칙 (단답형)
+
+여러 유효한 블록이 동시에 제안되면 **포크(Fork)**가 발생합니다.
+이더리움의 LMD-GHOST(Latest Message Driven GHOST) 규칙은 어떻게 "정규 체인"을 선택하나요?
+
+1) LMD-GHOST의 기본 원리는 무엇인가요?
+2) **왜** "가장 최근 메시지"를 사용하나요? (오래된 메시지를 사용하면 어떤 문제가?)
+
+**답변:**
+
+1) LMD-GHOST 원리:
+ 각 검증자의 가장 최근 증명(attestation)만 고려하여, 포크가 발생했을 때 가장 많은 증명 가중치(스테이킹 양)가 축적된 포크를 정규 체인으로 선택한다.
+
+2) 최근 메시지 사용 이유:
+ 오래된 메시지를 사용하면 검증자가 과거 투표를 재활용하여 이미 폐기된 포크에 허위 지지를 몰아줄 수 있다. 최신 메시지만 반영해야 검증자의 현재 의사를 정확히 반영하고 조작을 방지할 수 있다.
+
+
+---
+
+## 문제 6: RainbowKit Provider 계층 (빈칸 채우기)
+
+다음 코드의 빈칸을 채워서 RainbowKit을 올바르게 설정하세요.
+**Provider 순서가 중요합니다!**
+
+```typescript
+'use client';
+
+// TODO: 필요한 스타일 import
+_________________________________________
+
+import { RainbowKitProvider } from '@rainbow-me/rainbowkit';
+import { WagmiProvider } from 'wagmi';
+import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
+import { config } from '@/config/wagmi';
+
+const queryClient = new QueryClient();
+
+export default function RootLayout({ children }) {
+ return (
+
+
+ {/* TODO: Provider를 올바른 순서로 중첩하세요 */}
+ <_________________ config={config}>
+ <_________________ client={queryClient}>
+ <_________________>
+ {children}
+
+
+
+
+
+ );
+}
+```
+
+**답변:**
+```typescript
+'use client';
+
+import '@rainbow-me/rainbowkit/styles.css';
+
+import { RainbowKitProvider } from '@rainbow-me/rainbowkit';
+import { WagmiProvider } from 'wagmi';
+import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
+import { config } from '@/config/wagmi';
+
+const queryClient = new QueryClient();
+
+export default function RootLayout({ children }) {
+ return (
+
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
+```
+
+**왜 이 순서인가요:**
+WagmiProvider가 가장 바깥에 있어야 블록체인 연결 설정을 하위 모든 Provider와 컴포넌트에 제공한다. QueryClientProvider는 wagmi의 데이터 패칭/캐싱을 처리해야 하므로 그 안에 위치한다. RainbowKitProvider는 wagmi와 react-query 모두에 의존하므로 가장 안쪽에 위치해야 한다. 순서가 잘못되면 Context를 찾을 수 없다는 런타임 에러가 발생한다.
+
+
+---
+
+## 문제 7: Provider 순서 버그 (취약점 찾기)
+
+다음 코드에서 **문제점**을 찾고 수정하세요:
+
+```typescript
+// BAD CODE - 문제점 찾기
+'use client';
+
+import '@rainbow-me/rainbowkit/styles.css';
+import { RainbowKitProvider } from '@rainbow-me/rainbowkit';
+import { WagmiProvider } from 'wagmi';
+import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
+import { config } from '@/config/wagmi';
+
+const queryClient = new QueryClient();
+
+export default function Providers({ children }) {
+ return (
+ // 문제가 있는 Provider 순서!
+
+
+
+ {children}
+
+
+
+ );
+}
+```
+
+**1) 발견한 문제점:**
+Provider 순서가 완전히 뒤집혀 있다. WagmiProvider가 가장 안쪽에 있고, RainbowKitProvider가 WagmiProvider 바깥에 있다.
+
+**2) 왜 이것이 문제인가:**
+RainbowKitProvider는 내부적으로 wagmi의 Context에 의존하는데, WagmiProvider보다 바깥에 있으므로 WagmiContext를 찾을 수 없다. 마찬가지로 wagmi 훅들은 QueryClient에 의존하는데 WagmiProvider 안쪽에 QueryClientProvider가 없으므로 데이터 패칭이 실패한다.
+
+**3) 올바른 수정 방법:**
+```typescript
+'use client';
+
+import '@rainbow-me/rainbowkit/styles.css';
+import { RainbowKitProvider } from '@rainbow-me/rainbowkit';
+import { WagmiProvider } from 'wagmi';
+import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
+import { config } from '@/config/wagmi';
+
+const queryClient = new QueryClient();
+
+export default function Providers({ children }) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
+```
+
+---
+
+## 문제 8: 트랜잭션 상태 처리 (빈칸 채우기)
+
+다음 코드의 빈칸을 채워서 트랜잭션 전송 후 **확인 상태를 추적**하세요:
+
+```typescript
+'use client';
+
+import { useWriteContract, _________________ } from 'wagmi';
+
+const abi = [
+ {
+ name: 'increment',
+ type: 'function',
+ stateMutability: 'nonpayable',
+ inputs: [],
+ outputs: [],
+ },
+] as const;
+
+function IncrementButton() {
+ const { writeContract, data: hash, isPending } = useWriteContract();
+
+ // TODO: 트랜잭션 확인 상태를 추적하는 hook
+ const { isLoading: isConfirming, isSuccess } = _________________({
+ _________________,
+ });
+
+ return (
+
+
+ writeContract({
+ address: '0x1234...5678',
+ abi,
+ functionName: 'increment',
+ })
+ }
+ disabled={isPending || isConfirming}
+ >
+ {isPending ? '서명 대기 중...' : isConfirming ? '확인 중...' : '증가'}
+
+
+ {isSuccess &&
트랜잭션 성공!
}
+
+ );
+}
+```
+
+**답변:**
+```typescript
+'use client';
+
+import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
+
+const abi = [
+ {
+ name: 'increment',
+ type: 'function',
+ stateMutability: 'nonpayable',
+ inputs: [],
+ outputs: [],
+ },
+] as const;
+
+function IncrementButton() {
+ const { writeContract, data: hash, isPending } = useWriteContract();
+
+ const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
+ hash,
+ });
+
+ return (
+
+
+ writeContract({
+ address: '0x1234...5678',
+ abi,
+ functionName: 'increment',
+ })
+ }
+ disabled={isPending || isConfirming}
+ >
+ {isPending ? '서명 대기 중...' : isConfirming ? '확인 중...' : '증가'}
+
+
+ {isSuccess &&
트랜잭션 성공!
}
+
+ );
+}
+```
+
+**트랜잭션 상태 흐름을 설명하세요:**
+
+1) isPending 상태: 사용자가 지갑(MetaMask 등)에서 트랜잭션 서명을 승인하기를 기다리는 단계이다.
+2) isConfirming 상태: 서명이 완료되어 트랜잭션이 네트워크에 전송된 후, 블록에 포함(채굴)되기를 기다리는 단계이다.
+3) isSuccess 상태: 트랜잭션이 블록에 포함되어 온체인에서 성공적으로 실행 완료된 상태이다.
+
+
+---
+
+## 문제 9: 검증자 생애주기 (다이어그램 해석)
+
+다음 다이어그램은 이더리움 검증자의 생애주기를 보여줍니다:
+
+```mermaid
+stateDiagram-v2
+ [*] --> Pending: 32 ETH 입금
+ Pending --> Active: 활성화 큐 대기
+ Active --> Slashed: 규칙 위반
+ Active --> Exiting: 자발적 종료
+ Exiting --> Exited: 출금 대기
+ Slashed --> Exited: 강제 퇴장
+ Exited --> [*]: ETH 출금
+```
+
+**질문:**
+
+1) **Active** 상태에서 검증자가 수행하는 주요 활동은 무엇인가요?
+
+블록을 제안(Proposing)하고, 다른 검증자가 제안한 블록의 유효성을 증명(Attesting)하는 것이다. 이를 통해 네트워크의 합의에 참여하고 보상을 받는다.
+
+2) Active에서 **Slashed**로 전이되는 조건은 무엇인가요? 이 경우 검증자에게 어떤 일이 발생하나요?
+
+이중 투표(같은 슬롯에 두 블록 투표)나 서라운드 투표(모순된 증명 제출) 등의 규칙 위반 시 전이된다. 스테이킹한 ETH의 일부가 강제 소각되고, 검증자는 네트워크에서 강제 퇴장당한다.
+
+3) 검증자가 자발적으로 종료(**Exiting**)하려면 왜 바로 ETH를 출금할 수 없고 대기 기간이 필요한가요?
+
+대기 기간이 있어야 해당 검증자가 퇴장 전에 악의적 행동을 하지 않았는지를 네트워크가 검증할 수 있다. 또한 다수의 검증자가 동시에 빠져나가면 네트워크 보안이 약화되므로, 퇴장 속도를 조절하여 네트워크 안정성을 유지하기 위함이다.
+
+
+---
+
+## 문제 10: Provider 계층 구조 (다이어그램 해석)
+
+다음 다이어그램은 RainbowKit/wagmi 앱의 Provider 구조를 보여줍니다:
+
+```mermaid
+graph TD
+ subgraph App["React App"]
+ WP["WagmiProvider config 제공"]
+ QP["QueryClientProvider 캐싱/상태관리"]
+ RP["RainbowKitProvider 지갑 UI"]
+ COMP["Components useAccount, useWriteContract 등"]
+ end
+
+ WP --> QP --> RP --> COMP
+
+ subgraph Deps["의존성"]
+ CONFIG["wagmi config"]
+ QC["QueryClient"]
+ WALLET["지갑 연결 상태"]
+ end
+
+ CONFIG -.-> WP
+ QC -.-> QP
+ WP -.-> RP
+ QP -.-> COMP
+```
+
+**질문:**
+
+1) **WagmiProvider**가 가장 바깥에 있어야 하는 이유는 무엇인가요?
+
+WagmiProvider는 블록체인 연결 설정(chain, transport 등)을 React Context로 제공하며, QueryClientProvider와 RainbowKitProvider 모두 이 Context에 의존하기 때문이다. 가장 바깥에 있어야 하위 모든 컴포넌트에서 wagmi 훅을 사용할 수 있다.
+
+2) **QueryClientProvider**의 역할은 무엇인가요? 없다면 어떤 문제가 발생하나요?
+
+QueryClientProvider는 TanStack Query(react-query)의 캐싱과 비동기 상태 관리를 담당한다. 없으면 wagmi의 데이터 패칭 훅(useBalance, useReadContract 등)이 동작하지 않고, 캐싱이 불가능하여 매번 중복 요청이 발생한다.
+
+3) 아래 코드에서 `useAccount()` hook이 **"Cannot find WagmiContext"** 오류를 발생시키는 이유는 무엇인가요?
+
+```typescript
+// 오류 발생 코드
+
+
+ {/* WagmiProvider가 안쪽에 있음 */}
+ {/* useAccount() 호출 */}
+
+
+
+```
+
+RainbowKitProvider가 WagmiProvider보다 바깥에 위치해 있기 때문이다. RainbowKitProvider 내부에서 wagmi의 Context를 참조하려 하지만, 아직 WagmiProvider가 렌더링되지 않은 상태이므로 WagmiContext를 찾을 수 없다는 에러가 발생한다.
+
+
+---
+
+## 제출 전 체크리스트
+
+- [x] 모든 문제에 답변을 작성했는가?
+- [x] 객관식 문제: 정답 선택 **이유**를 설명했는가?
+- [x] 단답형 문제: 2-3문장 이상으로 충분히 설명했는가?
+- [x] 코드 문제: 완성된 코드와 **왜 그렇게 작성했는지** 설명했는가?
+- [x] 다이어그램 문제: 각 질문에 논리적으로 답변했는가?