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 ( +
+
+ + + +
+ +
+ ) +} 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 ( +
+

Loading...

+
+ ) + } + + 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 ( +
+

연결된 주소: {address}

+
+ ); +} +``` + +**답변:** +```typescript +import { useAccount } from 'wagmi'; + +function WalletStatus() { + // TODO: useAccount hook에서 필요한 값들을 가져오세요 + const { isConnected, address } = useAccount(); + + if (!isConnected) { + return
지갑이 연결되지 않았습니다
; + } + + return ( +
+

연결된 주소: {address}

+
+ ); +} +``` + +**왜 이렇게 작성했나요:** + + +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 백엔드 개발 **답변:** - - ---