Guider/Blockchain/BlockchainDevGuide0005
Blockchain#05

BlockchainDevGuide0005

NFT·DeFi 심화 완전 정복

⟠ BLOCKCHAIN DEV GUIDE SERIES

NFT · DeFi 심화 완전 정복 A-Z

ERC-1155 · NFT 마켓플레이스 · Uniswap V3 · Aave · DAO · Chainlink · MEV 방어

Guide 0005학습 기간: 2~3주난이도: 고급

📍 시리즈 진행 현황

✅ Guide0001 블록체인 개념✅ Guide0002 프로그래밍 기초✅ Guide0003 스마트 컨트랙트✅ Guide0004 개발환경·배포🔥 Guide0005 NFT·DeFi ← 현재⏳ Guide0006 보안·감사⏳ Guide0007 실전 프로젝트

📚 전체 학습 목차

챕터 주제 핵심 키워드 난이도
01 ERC-1155 멀티토큰 배치 전송, 게임 아이템, FT+NFT ★★★☆☆
02 NFT 마켓플레이스 EIP-2981 로열티, Escrow, 더치 경매 ★★★★☆
03 Uniswap V3 DEX 집중 유동성, 틱, 수수료 티어 ★★★★☆
04 Aave 대출 프로토콜 플래시론, 담보, 청산, aToken ★★★★★
05 DAO 거버넌스 Governor, 투표, Timelock ★★★★☆
06 Chainlink 오라클 Price Feed, VRF, CCIP ★★★☆☆
07 크로스체인 브릿지 CCIP, LayerZero, 보안 ★★★★★
08 MEV 프론트러닝 방어 Flashbots, Commit-Reveal ★★★★★
09 풀스택 DApp 아키텍처 Next.js 14, wagmi v2, The Graph ★★★★☆
10 현업 면접 Q&A TOP 15 실무 질문, 답변 전략 ★★★☆☆

Chapter 01

ERC-1155 멀티토큰 표준 완전 정복

하나의 컨트랙트로 FT와 NFT 동시 관리. 배치 전송으로 가스를 최대 96% 절약합니다.

1-1. ERC-20 / ERC-721 / ERC-1155 완전 비교표

구분 ERC-20 ERC-721 ERC-1155
토큰 종류 대체 가능(FT) 대체 불가(NFT) FT+NFT 동시
배치 전송 ✅ 가스 96%↓
메타데이터 없음 tokenURI(id) uri(id) 동적
주요 사례 DeFi 토큰 디지털 아트 게임/메타버스

1-2. 게임 아이템 컨트랙트 완전 구현

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract GameItems is ERC1155, Ownable {
uint256 public constant GOLD = 0; // FT 금화
uint256 public constant MANA_STONE = 1; // FT 마나스톤
uint256 public constant SWORD_LEGEND = 100; // NFT 전설검
struct ItemStats { uint256 atk; uint256 def; uint8 rarity; }
mapping(uint256 => ItemStats) public stats;
// 배치 민팅 — 한 tx에 여러 토큰 지급
function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts) external onlyOwner {
_mintBatch(to, ids, amounts, "");
}
// 토큰별 다른 메타데이터 URI
function uri(uint256 id) public view override returns(string memory) {
return string(abi.encodePacked(super.uri(0), Strings.toString(id), ".json"));
}
}

Chapter 02

NFT 마켓플레이스 완전 구현

OpenSea처럼 NFT를 리스팅·구매·경매하는 탈중앙 마켓플레이스. EIP-2981 로열티와 더치 경매까지 완전 구현합니다.

2-1. 마켓플레이스 아키텍처

컴포넌트 역할 패턴
NFTMarketplace.sol 리스팅·구매·취소·로열티 ReentrancyGuard + CEI
AuctionHouse.sol 영국식·더치 경매 Commit-Reveal + Escrow
RoyaltyRegistry.sol EIP-2981 로열티 관리 Registry 패턴

2-2. 핵심 구매 함수 — CEI 패턴 + 로열티

struct Listing { address seller; address nftContract; uint256 tokenId; uint256 price; bool active; }
mapping(bytes32 => Listing) public listings;
function buyNFT(bytes32 listingId) external payable nonReentrant {
Listing storage l = listings[listingId];
require(l.active, "Not active");
require(msg.value >= l.price, "Insufficient ETH");
// ① CEI: 상태 먼저 변경 (재진입 방지)
l.active = false;
// ② EIP-2981: 로열티 자동 조회
(address royaltyReceiver, uint256 royaltyAmt) = IERC2981(l.nftContract).royaltyInfo(l.tokenId, l.price);
// ③ 수수료 2.5% + 로열티 차감 후 판매자에게
uint256 fee = l.price * 250 / 10000;
payable(royaltyReceiver).transfer(royaltyAmt);
payable(l.seller).transfer(l.price - fee - royaltyAmt);
IERC721(l.nftContract).safeTransferFrom(l.seller, msg.sender, l.tokenId);
}

2-3. 더치 경매(Dutch Auction) 구현

더치 경매란? 높은 가격에서 시작해 시간이 지날수록 낮아지는 방식. 구매자는 원하는 가격이 되는 순간 구매. NFT 민팅의 공정한 가격 발견에 자주 사용됩니다.

contract DutchAuction {
uint256 public immutable startPrice; // 시작가
uint256 public immutable discountRate; // 초당 할인액
uint256 public immutable startAt;
function getPrice() public view returns(uint256) {
return startPrice - discountRate * (block.timestamp - startAt);
}
function buy() external payable {
uint256 price = getPrice();
require(msg.value >= price, "ETH too low");
// NFT 전송 + 잔금 환불
if (msg.value - price > 0) payable(msg.sender).transfer(msg.value - price);
}
}

📘 Chapter 03

Uniswap V3 — 집중 유동성 DEX 완전 정복

자동화 마켓메이커(AMM)의 진화 최종판, Uniswap V3의 핵심 메커니즘부터 직접 구현까지

🎯 학습 목표

  • Uniswap V1/V2/V3 진화 과정과 핵심 차이 이해
  • 집중 유동성(Concentrated Liquidity) 수학적 원리 파악
  • Tick, Position, Slot0 등 핵심 구조체 완전 이해
  • LP Position NFT(ERC-721) 발행 메커니즘 구현
  • 멀티홉 라우팅 및 플래시 스왑 구현
  • 실제 Uniswap V3 Fork 프로젝트 배포

3.1 AMM 진화사 — V1에서 V3까지

버전 출시 핵심 공식 특징 한계
V1 2018.11 x * y = k ETH-Token 페어만 지원, 수수료 0.3% Token-Token은 ETH 경유, 자본 비효율
V2 2020.05 x * y = k Token-Token 직접 페어, 플래시스왑 유동성이 전 가격 범위에 분산 → 자본비효율
V3 2021.05 √(x*y) = L 집중 유동성, 다중 수수료 티어, NFT LP 복잡성 증가, 능동적 관리 필요

3.2 집중 유동성(Concentrated Liquidity) 원리

/* V2: 유동성이 [0, ∞) 전 범위에 분산 → 대부분의 자금이 실제 거래가 일어나지 않는 가격대에 잠김 */

// V3 핵심 개념: LP가 [tickLower, tickUpper] 범위만 지정 → 자본효율 최대 4000배 향상

// 가격 = 1.0001^tick (tick은 정수, 각 tick은 0.01% 가격 변화)

💡 tick이란?

Uniswap V3는 가격 공간을 tick이라는 정수 단위로 이산화합니다. price = 1.0001^tick으로 정의되어, tick이 1 증가할 때마다 가격이 약 0.01% 상승합니다. LP는 자신이 원하는 가격 구간(tickLower ~ tickUpper)만 지정해 유동성을 공급하여 자본 효율성을 극대화합니다.

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.19;

import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/**
 * @title UniswapV3LiquidityProvider
 * @dev Uniswap V3 집중 유동성 공급 실전 예제
 *      - 원하는 가격 범위에만 유동성 공급
 *      - LP Position은 NFT(ERC-721)로 표현됨
 *      - 수수료 청구 및 유동성 제거 포함
 */
contract UniswapV3LiquidityProvider {
    using SafeERC20 for IERC20;

    INonfungiblePositionManager public immutable nonfungiblePositionManager;
    
    // 수수료 티어 상수
    uint24 public constant FEE_LOW    = 500;   // 0.05% — 안정적 쌍 (USDC/USDT)
    uint24 public constant FEE_MEDIUM = 3000;  // 0.30% — 일반 쌍 (ETH/USDC)
    uint24 public constant FEE_HIGH   = 10000; // 1.00% — 변동성 높은 쌍

    struct DepositInfo {
        address owner;
        uint128 liquidity;
        address token0;
        address token1;
    }

    // tokenId → DepositInfo
    mapping(uint256 => DepositInfo) public deposits;

    event LiquidityAdded(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
    event LiquidityRemoved(uint256 indexed tokenId, uint256 amount0, uint256 amount1);
    event FeesCollected(uint256 indexed tokenId, uint256 amount0, uint256 amount1);

    constructor(address _nonfungiblePositionManager) {
        nonfungiblePositionManager = INonfungiblePositionManager(_nonfungiblePositionManager);
    }

    /**
     * @notice 집중 유동성 공급 — 특정 가격 범위에만 유동성 제공
     * @param token0     첫 번째 토큰 주소
     * @param token1     두 번째 토큰 주소
     * @param fee        수수료 티어 (500 / 3000 / 10000)
     * @param tickLower  하한 tick (가격 하한)
     * @param tickUpper  상한 tick (가격 상한)
     * @param amount0    공급할 token0 최대량
     * @param amount1    공급할 token1 최대량
     */
    function mintPosition(
        address token0,
        address token1,
        uint24  fee,
        int24   tickLower,
        int24   tickUpper,
        uint256 amount0,
        uint256 amount1
    ) external returns (uint256 tokenId, uint128 liquidity, uint256 amount0Used, uint256 amount1Used) {
        // 토큰 이전
        IERC20(token0).safeTransferFrom(msg.sender, address(this), amount0);
        IERC20(token1).safeTransferFrom(msg.sender, address(this), amount1);

        // Position Manager에 approve
        IERC20(token0).approve(address(nonfungiblePositionManager), amount0);
        IERC20(token1).approve(address(nonfungiblePositionManager), amount1);

        // 유동성 민팅 (NFT LP 토큰 발행)
        INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({
            token0:      token0,
            token1:      token1,
            fee:         fee,
            tickLower:   tickLower,
            tickUpper:   tickUpper,
            amount0Desired: amount0,
            amount1Desired: amount1,
            amount0Min:  0,              // 실제에서는 슬리피지 보호 필요
            amount1Min:  0,
            recipient:   address(this),  // 이 컨트랙트가 NFT 보유
            deadline:    block.timestamp + 15 minutes
        });

        (tokenId, liquidity, amount0Used, amount1Used) = nonfungiblePositionManager.mint(params);

        // 잔여 토큰 반환
        if (amount0 > amount0Used) IERC20(token0).safeTransfer(msg.sender, amount0 - amount0Used);
        if (amount1 > amount1Used) IERC20(token1).safeTransfer(msg.sender, amount1 - amount1Used);

        // 포지션 정보 저장
        deposits[tokenId] = DepositInfo({
            owner:     msg.sender,
            liquidity: liquidity,
            token0:    token0,
            token1:    token1
        });

        emit LiquidityAdded(tokenId, liquidity, amount0Used, amount1Used);
    }

    /**
     * @notice 누적 수수료 청구
     */
    function collectFees(uint256 tokenId) external returns (uint256 amount0, uint256 amount1) {
        require(deposits[tokenId].owner == msg.sender, "NOT_OWNER");

        INonfungiblePositionManager.CollectParams memory params = INonfungiblePositionManager.CollectParams({
            tokenId:    tokenId,
            recipient:  msg.sender,
            amount0Max: type(uint128).max,
            amount1Max: type(uint128).max
        });

        (amount0, amount1) = nonfungiblePositionManager.collect(params);
        emit FeesCollected(tokenId, amount0, amount1);
    }

    /**
     * @notice 유동성 제거 + 수수료 동시 청구
     */
    function removeLiquidity(uint256 tokenId) external returns (uint256 amount0, uint256 amount1) {
        require(deposits[tokenId].owner == msg.sender, "NOT_OWNER");

        uint128 liquidity = deposits[tokenId].liquidity;

        // 유동성 소각
        INonfungiblePositionManager.DecreaseLiquidityParams memory decParams =
            INonfungiblePositionManager.DecreaseLiquidityParams({
                tokenId:    tokenId,
                liquidity:  liquidity,
                amount0Min: 0,
                amount1Min: 0,
                deadline:   block.timestamp + 15 minutes
            });
        nonfungiblePositionManager.decreaseLiquidity(decParams);

        // 수수료 포함 전액 청구
        INonfungiblePositionManager.CollectParams memory colParams = INonfungiblePositionManager.CollectParams({
            tokenId:    tokenId,
            recipient:  msg.sender,
            amount0Max: type(uint128).max,
            amount1Max: type(uint128).max
        });

        (amount0, amount1) = nonfungiblePositionManager.collect(colParams);

        // NFT 소각
        nonfungiblePositionManager.burn(tokenId);
        delete deposits[tokenId];

        emit LiquidityRemoved(tokenId, amount0, amount1);
    }
}

3.3 멀티홉 스왑 라우팅

🔀 멀티홉이란?

A→B→C처럼 중간 토큰을 거쳐 스왑하는 방식. 예: USDC → ETH → WBTC를 단일 트랜잭션으로 처리. SwapRouter의 exactInputSingle(단일홉) vs exactInput(멀티홉)으로 구현합니다.

// SwapRouter 인터페이스 활용 — 멀티홉 스왑 예제
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";

contract UniswapV3Swapper {
    ISwapRouter public immutable swapRouter;
    address public constant WETH9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    constructor(address _swapRouter) {
        swapRouter = ISwapRouter(_swapRouter);
    }

    // 단일홉: USDC → ETH
    function swapExactInputSingle(
        address tokenIn,
        address tokenOut,
        uint24  fee,
        uint256 amountIn,
        uint256 amountOutMinimum
    ) external returns (uint256 amountOut) {
        IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
        IERC20(tokenIn).approve(address(swapRouter), amountIn);

        ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
            tokenIn:           tokenIn,
            tokenOut:          tokenOut,
            fee:               fee,
            recipient:         msg.sender,
            deadline:          block.timestamp,
            amountIn:          amountIn,
            amountOutMinimum:  amountOutMinimum,
            sqrtPriceLimitX96: 0
        });

        amountOut = swapRouter.exactInputSingle(params);
    }

    // 멀티홉: USDC → WETH → WBTC
    // path = abi.encodePacked(USDC, fee1, WETH, fee2, WBTC)
    function swapExactInputMultiHop(
        bytes calldata path,
        address tokenIn,
        uint256 amountIn,
        uint256 amountOutMinimum
    ) external returns (uint256 amountOut) {
        IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
        IERC20(tokenIn).approve(address(swapRouter), amountIn);

        ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({
            path:             path,
            recipient:        msg.sender,
            deadline:         block.timestamp,
            amountIn:         amountIn,
            amountOutMinimum: amountOutMinimum
        });

        amountOut = swapRouter.exactInput(params);
    }
}

⚠️ Uniswap V3 현업 핵심 체크리스트

  • 슬리피지 보호: amountOutMinimum 반드시 설정 (0 금지)
  • deadline 설정: block.timestamp + N분 (무한정 대기 방지)
  • tick 정렬: tickLower < tickUpper, tickSpacing 배수여야 함
  • 토큰 정렬: token0 < token1 (주소 정수 비교)
  • 수수료 티어 선택: 안정 쌍=500, 일반=3000, 변동성=10000

📘 Chapter 04

Aave — 탈중앙화 대출 프로토콜 완전 정복

담보 대출, 플래시 론, 이자율 모델, 청산 메커니즘까지 — DeFi 대출의 핵심을 완전히 파악한다

🎯 학습 목표

  • Aave V2/V3 아키텍처와 핵심 컨트랙트 이해
  • aToken, debtToken, Health Factor 완전 이해
  • 이자율 모델(Linear/Jump Rate) 수학적 원리 파악
  • 플래시 론(Flash Loan) 구현 및 실전 활용
  • 청산(Liquidation) 메커니즘 구현
  • IFlashLoanReceiver 인터페이스 직접 구현

4.1 Aave 핵심 개념 — aToken & Health Factor

개념 설명 예시
aToken 예치 시 1:1로 발행되는 이자부 토큰. 시간이 지남에 따라 잔액이 증가 1000 USDC 예치 → 1000 aUSDC, 1년 후 → 1035 aUSDC
variableDebtToken 변동금리 대출 포지션 추적 토큰. 이자 누적에 따라 잔액 증가 500 USDC 대출 → 500 variableDebtUSDC 발행
LTV (Loan-to-Value) 담보 대비 최대 대출 가능 비율 ETH LTV 80% → 1ETH(1000$) 담보 시 최대 800$ 대출
Health Factor 청산 안전도 지표. 1.0 이하 시 청산 발생 HF = (담보가치 × 청산임계값) / 총부채. HF < 1 → 청산
Flash Loan 담보 없이 단일 트랜잭션 내 대출+상환. 수수료 0.05% 1M USDC 무담보 대출 → 차익거래 → 상환+500$ 수수료

4.2 플래시 론(Flash Loan) 완전 구현

⚡ 플래시 론 동작 원리

1. 사용자가 Aave에 플래시 론 요청 (담보 없음) → 2. Aave가 자금 전송 → 3. 사용자 로직 실행(차익거래, 청산, 레버리지 등) → 4. 원금+수수료 상환 → 5. 상환 실패 시 트랜잭션 전체 롤백 (원자성 보장)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@aave/core-v3/contracts/flashloan/base/FlashLoanSimpleReceiverBase.sol";
import "@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/**
 * @title AaveFlashLoanArbitrage
 * @dev Aave V3 Flash Loan을 이용한 DEX 차익거래 예제
 *      실제 수익이 발생하는 구조로 설계
 *
 * 시나리오: Uniswap에서 ETH가 저렴하고 Sushiswap에서 비쌀 때
 *   1. Aave에서 USDC 플래시 론
 *   2. Uniswap에서 USDC → ETH (저가 매수)
 *   3. Sushiswap에서 ETH → USDC (고가 매도)
 *   4. Aave에 원금 + 수수료 상환
 *   5. 차익 보관
 */
contract AaveFlashLoanArbitrage is FlashLoanSimpleReceiverBase {
    
    address public owner;
    
    // DEX 라우터 주소
    address public constant UNISWAP_ROUTER   = 0xE592427A0AEce92De3Edee1F18E0157C05861564;
    address public constant SUSHISWAP_ROUTER = 0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506;

    event FlashLoanExecuted(address token, uint256 amount, uint256 premium, uint256 profit);

    modifier onlyOwner() {
        require(msg.sender == owner, "NOT_OWNER");
        _;
    }

    constructor(address _addressProvider) 
        FlashLoanSimpleReceiverBase(IPoolAddressesProvider(_addressProvider)) 
    {
        owner = msg.sender;
    }

    /**
     * @notice 플래시 론 시작 — 외부에서 호출
     * @param asset  대출받을 토큰 (예: USDC)
     * @param amount 대출 금액
     */
    function requestFlashLoan(address asset, uint256 amount) external onlyOwner {
        address receiverAddress = address(this);
        bytes memory params = "";   // executeOperation에 추가 파라미터 전달 가능
        uint16 referralCode = 0;

        // Aave V3 Pool에 플래시 론 요청
        POOL.flashLoanSimple(receiverAddress, asset, amount, params, referralCode);
    }

    /**
     * @notice Aave가 자금을 보낸 후 자동으로 호출하는 콜백 함수
     *         이 함수 안에서 차익거래 로직 실행
     * @param asset     대출받은 토큰
     * @param amount    대출 금액
     * @param premium   수수료 (amount의 0.05%)
     * @param params    추가 파라미터
     */
    function executeOperation(
        address asset,
        uint256 amount,
        uint256 premium,
        address, /* initiator */
        bytes calldata params
    ) external override returns (bool) {
        require(msg.sender == address(POOL), "CALLER_MUST_BE_POOL");
        
        uint256 balanceBefore = IERC20(asset).balanceOf(address(this));
        
        // ========================================
        // 실제 차익거래 로직 (여기를 커스터마이즈)
        // ========================================
        _executeArbitrage(asset, amount);
        // ========================================
        
        uint256 balanceAfter = IERC20(asset).balanceOf(address(this));
        uint256 totalOwed = amount + premium;
        
        require(balanceAfter >= totalOwed, "INSUFFICIENT_FUNDS_TO_REPAY");
        
        // Aave Pool에 상환 승인
        IERC20(asset).approve(address(POOL), totalOwed);
        
        uint256 profit = balanceAfter - totalOwed;
        emit FlashLoanExecuted(asset, amount, premium, profit);
        
        return true; // true 반환 = 상환 승인
    }

    /**
     * @dev 차익거래 실행 내부 로직
     *      실제에서는 Uniswap/Sushiswap 라우터 호출
     */
    function _executeArbitrage(address asset, uint256 amount) internal {
        // 1. Uniswap에서 저가 매수 (USDC → ETH)
        // swapExactTokensForTokens(uniswapRouter, asset, WETH, amount);
        
        // 2. Sushiswap에서 고가 매도 (ETH → USDC)
        // swapExactTokensForTokens(sushiswapRouter, WETH, asset, wethAmount);
        
        // 실제 구현 시 각 DEX 라우터의 swap 함수 호출
        // 이 예제에서는 단순화를 위해 생략
    }

    // 수익 출금
    function withdrawProfit(address token) external onlyOwner {
        uint256 balance = IERC20(token).balanceOf(address(this));
        IERC20(token).transfer(owner, balance);
    }

    // ETH 수신 허용
    receive() external payable {}
}

4.3 청산(Liquidation) 봇 구현

// 청산자(Liquidator) 컨트랙트 — Health Factor < 1 인 포지션 청산
contract AaveLiquidator {
    IPool public immutable pool;
    
    // 청산 보너스 최대 15% — 이 수익으로 청산 인센티브 제공
    uint256 public constant LIQUIDATION_BONUS = 10500; // 105% = 5% 보너스
    
    /**
     * @notice 청산 실행
     * @param collateralAsset  청산할 담보 토큰 (받게 될 토큰)
     * @param debtAsset        갚아줄 부채 토큰
     * @param user             청산 대상 주소
     * @param debtToCover      갚아줄 부채량 (최대 50%까지)
     * @param receiveAToken    aToken으로 받을지 여부
     */
    function liquidate(
        address collateralAsset,
        address debtAsset,
        address user,
        uint256 debtToCover,
        bool receiveAToken
    ) external {
        // 부채 토큰 전송
        IERC20(debtAsset).transferFrom(msg.sender, address(this), debtToCover);
        IERC20(debtAsset).approve(address(pool), debtToCover);
        
        // 청산 실행 — debtToCover만큼 갚고, collateralAsset을 할인가에 획득
        pool.liquidationCall(collateralAsset, debtAsset, user, debtToCover, receiveAToken);
        
        // 획득한 담보 청산자에게 전송
        uint256 collateralBalance = IERC20(collateralAsset).balanceOf(address(this));
        IERC20(collateralAsset).transfer(msg.sender, collateralBalance);
    }
    
    // Health Factor 조회 헬퍼
    function getHealthFactor(address user) external view returns (uint256) {
        (, , , , , uint256 hf) = pool.getUserAccountData(user);
        return hf; // 1e18 단위, 1e18 = HF 1.0
    }
}

⚠️ Aave 현업 필수 체크리스트

  • Flash Loan 수수료: V3 기준 0.05% (aave governance로 변경 가능)
  • 청산 한도: 단일 청산 시 부채의 최대 50%만 청산 가능 (CLOSE_FACTOR)
  • 오라클 의존: Chainlink Price Feed 사용 — 오라클 조작 공격 유의
  • Isolation Mode (V3): 신규 자산은 격리 모드로만 담보 사용 가능
  • E-Mode (V3): 동일 카테고리 자산끼리 LTV 95%까지 효율 극대화

📘 Chapter 05

DAO 거버넌스 — 탈중앙화 자율조직 완전 구현

OpenZeppelin Governor를 활용한 온체인 투표 시스템, 타임락, 프로포절 라이프사이클 완전 정복

5.1 DAO 거버넌스 아키텍처

컴포넌트 역할 구현체
Governor 프로포절 생성, 투표 집계, 실행 조율 GovernorBravo, OpenZeppelin Governor
Governance Token 투표권 부여, 위임 기능 ERC20Votes (EIP-5805)
TimelockController 가결된 프로포절 실행 전 대기 (보안) OpenZeppelin TimelockController
Treasury DAO 자금 보관, TimelockController가 소유 ETH/토큰 보유 컨트랙트

🗳️ 프로포절 라이프사이클

Pending (생성 후 대기) → Active (투표 기간) → Succeeded/Defeated (결과) → Queued (Timelock 대기) → Executed (실행) / Canceled (취소)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";

/**
 * @title MyDAO
 * @dev 완전한 DAO 거버넌스 시스템
 *      - ERC20Votes 토큰으로 투표권 부여
 *      - 투표 지연: 1일 (약 7200 블록)
 *      - 투표 기간: 1주일 (약 50400 블록)
 *      - 최소 정족수: 전체 공급량의 4%
 *      - 통과 임계값: 과반수 (50% 초과)
 *      - Timelock: 2일 대기 후 실행
 */
contract MyDAO is
    Governor,
    GovernorSettings,
    GovernorCountingSimple,
    GovernorVotes,
    GovernorVotesQuorumFraction,
    GovernorTimelockControl
{
    constructor(
        IVotes _token,
        TimelockController _timelock
    )
        Governor("MyDAO")
        GovernorSettings(
            7200,   // votingDelay: 1 day
            50400,  // votingPeriod: 1 week
            0       // proposalThreshold: 0 tokens (누구나 프로포절 가능)
        )
        GovernorVotes(_token)
        GovernorVotesQuorumFraction(4)   // 4% 정족수
        GovernorTimelockControl(_timelock)
    {}

    // ========== 충돌 해결 (다중 상속) ==========

    function votingDelay() public view override(IGovernor, GovernorSettings) returns (uint256) {
        return super.votingDelay();
    }

    function votingPeriod() public view override(IGovernor, GovernorSettings) returns (uint256) {
        return super.votingPeriod();
    }

    function quorum(uint256 blockNumber)
        public view override(IGovernor, GovernorVotesQuorumFraction) returns (uint256) {
        return super.quorum(blockNumber);
    }

    function state(uint256 proposalId)
        public view override(Governor, GovernorTimelockControl) returns (ProposalState) {
        return super.state(proposalId);
    }

    function propose(
        address[] memory targets,
        uint256[] memory values,
        bytes[] memory calldatas,
        string memory description
    ) public override(Governor, IGovernor) returns (uint256) {
        return super.propose(targets, values, calldatas, description);
    }

    function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) {
        return super.proposalThreshold();
    }

    function _execute(
        uint256 proposalId,
        address[] memory targets,
        uint256[] memory values,
        bytes[] memory calldatas,
        bytes32 descriptionHash
    ) internal override(Governor, GovernorTimelockControl) {
        super._execute(proposalId, targets, values, calldatas, descriptionHash);
    }

    function _cancel(
        address[] memory targets,
        uint256[] memory values,
        bytes[] memory calldatas,
        bytes32 descriptionHash
    ) internal override(Governor, GovernorTimelockControl) returns (uint256) {
        return super._cancel(targets, values, calldatas, descriptionHash);
    }

    function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) {
        return super._executor();
    }

    function supportsInterface(bytes4 interfaceId)
        public view override(Governor, GovernorTimelockControl) returns (bool) {
        return super.supportsInterface(interfaceId);
    }
}

// ===== 거버넌스 토큰 =====
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";

contract GovernanceToken is ERC20, ERC20Permit, ERC20Votes {
    constructor() ERC20("MyDAO Token", "MDT") ERC20Permit("MyDAO Token") {
        _mint(msg.sender, 1_000_000 * 10**18); // 100만 토큰
    }

    // ERC20Votes는 투표 전 반드시 delegate() 호출 필요!
    // delegate(address(self)) — 자기 자신에게 위임
    // delegate(address(other)) — 다른 주소에 위임

    function _afterTokenTransfer(address from, address to, uint256 amount)
        internal override(ERC20, ERC20Votes) { super._afterTokenTransfer(from, to, amount); }

    function _mint(address to, uint256 amount)
        internal override(ERC20, ERC20Votes) { super._mint(to, amount); }

    function _burn(address account, uint256 amount)
        internal override(ERC20, ERC20Votes) { super._burn(account, amount); }
}

// ===== Timelock Controller =====
import "@openzeppelin/contracts/governance/TimelockController.sol";

// 배포 시:
// new TimelockController(
//   2 days,            // minDelay: 2일 후 실행
//   [governorAddress], // proposers: Governor만 대기열 추가 가능
//   [address(0)],      // executors: 누구나 실행 가능 (address(0) = 모두)
//   msg.sender         // admin: 초기 admin (이후 포기 권장)
// )

5.2 DAO 실전 — 프로포절 생성부터 실행까지 (ethers.js)

// DAO 운영 전체 플로우 — Hardhat 스크립트
const { ethers } = require("hardhat");

async function runDAOCycle() {
  const [proposer, voter1, voter2, voter3] = await ethers.getSigners();
  
  // 컨트랙트 연결
  const token     = await ethers.getContractAt("GovernanceToken", TOKEN_ADDRESS);
  const governor  = await ethers.getContractAt("MyDAO", GOVERNOR_ADDRESS);
  const timelock  = await ethers.getContractAt("TimelockController", TIMELOCK_ADDRESS);
  const treasury  = await ethers.getContractAt("Treasury", TREASURY_ADDRESS);

  // === STEP 1: 토큰 위임 (필수! 투표 전 반드시 실행) ===
  await token.connect(voter1).delegate(voter1.address);
  await token.connect(voter2).delegate(voter2.address);
  await token.connect(voter3).delegate(voter3.address);
  console.log("✅ 투표권 위임 완료");

  // === STEP 2: 프로포절 생성 ===
  // 목표: Treasury에서 1 ETH를 제안자에게 지급
  const transferCalldata = treasury.interface.encodeFunctionData("release", [
    ethers.constants.AddressZero, // ETH
    proposer.address,
    ethers.utils.parseEther("1")
  ]);

  const proposeTx = await governor.propose(
    [treasury.address],    // 실행할 컨트랙트
    [0],                   // ETH value
    [transferCalldata],    // 호출 데이터
    "Proposal #1: 1 ETH 지급 for 개발 기여" // 설명
  );
  const receipt = await proposeTx.wait();
  const proposalId = receipt.events[0].args.proposalId;
  console.log("✅ 프로포절 ID:", proposalId.toString());

  // === STEP 3: 투표 대기 (votingDelay 블록 경과 후 투표 가능) ===
  await mineBlocks(7200 + 1); // 1일 대기 시뮬레이션

  // === STEP 4: 투표 실행 ===
  // 0=반대, 1=찬성, 2=기권
  await governor.connect(voter1).castVote(proposalId, 1); // 찬성
  await governor.connect(voter2).castVote(proposalId, 1); // 찬성
  await governor.connect(voter3).castVote(proposalId, 0); // 반대
  console.log("✅ 투표 완료");

  // === STEP 5: 투표 기간 종료 대기 ===
  await mineBlocks(50400 + 1);

  // === STEP 6: Timelock 대기열에 추가 ===
  const descriptionHash = ethers.utils.id("Proposal #1: 1 ETH 지급 for 개발 기여");
  await governor.queue([treasury.address], [0], [transferCalldata], descriptionHash);
  console.log("✅ Timelock 대기열 등록");

  // === STEP 7: Timelock 만료 대기 후 실행 ===
  await ethers.provider.send("evm_increaseTime", [2 * 24 * 60 * 60]); // 2일
  await governor.execute([treasury.address], [0], [transferCalldata], descriptionHash);
  console.log("✅ 프로포절 실행 완료!");
}

async function mineBlocks(n) {
  for(let i = 0; i < n; i++) {
    await ethers.provider.send("evm_mine", []);
  }
}

📘 Chapter 06

Chainlink 오라클 — 오프체인 데이터를 온체인으로

Price Feed, VRF, Automation, CCIP — Chainlink 생태계 완전 정복

6.1 Chainlink 4대 서비스

서비스 기능 활용 사례
Price Feeds 탈중앙화 가격 오라클 Aave, Compound 담보 가치 산정
VRF v2 검증 가능한 온체인 난수 NFT 랜덤 민팅, 게임 아이템
Automation 조건부 자동 트랜잭션 실행 청산 봇, 재투자, 만료 처리
CCIP 크로스체인 메시지/토큰 전송 멀티체인 DApp, 브릿지
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";

// ===== Price Feed =====
contract ChainlinkPriceFeed {
    AggregatorV3Interface internal priceFeed;

    // Mainnet ETH/USD: 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419
    constructor(address _priceFeed) {
        priceFeed = AggregatorV3Interface(_priceFeed);
    }

    function getLatestPrice() public view returns (int256 price, uint8 decimals, uint256 updatedAt) {
        (
            uint80 roundId,
            int256 answer,
            uint256 startedAt,
            uint256 updatedAt_,
            uint80 answeredInRound
        ) = priceFeed.latestRoundData();

        // 데이터 신선도 검증 (1시간 이상 오래된 데이터 거부)
        require(updatedAt_ > block.timestamp - 1 hours, "STALE_PRICE");
        require(answer > 0, "INVALID_PRICE");

        return (answer, priceFeed.decimals(), updatedAt_);
    }

    // USD 금액으로 ETH 수량 계산
    function getEthAmount(uint256 usdAmount) public view returns (uint256) {
        (int256 ethPrice, uint8 dec, ) = getLatestPrice();
        // ETH amount = USD / ETH_price
        return (usdAmount * 10**dec) / uint256(ethPrice);
    }
}

// ===== VRF v2 — 검증 가능한 난수 =====
contract RandomNFTMinter is VRFConsumerBaseV2 {
    VRFCoordinatorV2Interface immutable COORDINATOR;

    uint64  public subscriptionId;
    bytes32 public keyHash;     // gas lane
    uint32  public callbackGasLimit = 100000;
    uint16  public requestConfirmations = 3;

    // requestId → user
    mapping(uint256 => address) public requestToUser;
    // requestId → randomWords
    mapping(uint256 => uint256[]) public requestToRandomWords;

    event RandomRequested(uint256 indexed requestId, address indexed user);
    event NFTMinted(address indexed user, uint256 tokenId, string rarity);

    constructor(address vrfCoordinator, uint64 subId, bytes32 _keyHash)
        VRFConsumerBaseV2(vrfCoordinator)
    {
        COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
        subscriptionId = subId;
        keyHash = _keyHash;
    }

    // 난수 요청
    function requestRandomNFT() external returns (uint256 requestId) {
        requestId = COORDINATOR.requestRandomWords(
            keyHash,
            subscriptionId,
            requestConfirmations,
            callbackGasLimit,
            1   // numWords: 난수 1개 요청
        );
        requestToUser[requestId] = msg.sender;
        emit RandomRequested(requestId, msg.sender);
    }

    // VRF Coordinator가 난수 결과를 콜백으로 전달
    function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords)
        internal override
    {
        address user = requestToUser[requestId];
        uint256 randomNumber = randomWords[0] % 100; // 0-99 범위

        // 희귀도 결정 (확률 가중치)
        string memory rarity;
        if      (randomNumber < 5)  rarity = "Legendary";  // 5%
        else if (randomNumber < 20) rarity = "Epic";        // 15%
        else if (randomNumber < 50) rarity = "Rare";        // 30%
        else                        rarity = "Common";      // 50%

        uint256 tokenId = uint256(keccak256(abi.encodePacked(requestId, randomNumber)));
        // _mint(user, tokenId) — ERC721 민팅
        emit NFTMinted(user, tokenId, rarity);
    }
}

6.2 MEV(Maximal Extractable Value) 방어 전략

🚨 MEV란? — 보안 필수 지식

채굴자/검증자가 블록 내 트랜잭션 순서를 조작하여 이익을 추출하는 행위. 대표적 공격: 프런트런닝(내 주문 앞에 끼어들기), 샌드위치 공격(내 트랜잭션 앞뒤로 끼어들어 차익), 백런닝(내 주문 직후 실행).

// MEV 방어 기법 1: Commit-Reveal 패턴
contract CommitRevealBid {
    mapping(address => bytes32) public commits;
    mapping(address => uint256) public revealedBids;
    
    uint256 public commitDeadline;
    uint256 public revealDeadline;

    // Phase 1: 입찰가를 해시로 제출 (실제 값 숨김)
    function commit(bytes32 _hashedBid) external {
        require(block.timestamp < commitDeadline, "COMMIT_CLOSED");
        commits[msg.sender] = _hashedBid;
    }

    // Phase 2: 실제 값과 salt 공개 (해시 검증)
    function reveal(uint256 _bid, bytes32 _salt) external {
        require(block.timestamp >= commitDeadline, "REVEAL_NOT_STARTED");
        require(block.timestamp < revealDeadline, "REVEAL_CLOSED");
        
        // 제출한 해시와 공개한 값 검증
        bytes32 hash = keccak256(abi.encodePacked(_bid, _salt));
        require(hash == commits[msg.sender], "HASH_MISMATCH");
        
        revealedBids[msg.sender] = _bid;
        delete commits[msg.sender]; // 가스 환불
    }
}

// MEV 방어 기법 2: 슬리피지 보호 + 데드라인
function safeSwap(uint256 amountIn, uint256 minAmountOut, uint256 deadline) external {
    require(block.timestamp <= deadline, "EXPIRED");          // 시간 제한
    uint256 amountOut = _doSwap(amountIn);
    require(amountOut >= minAmountOut, "SLIPPAGE_EXCEEDED"); // 슬리피지 보호
}

// MEV 방어 기법 3: Flashbots private mempool 활용 (off-chain)
// 트랜잭션을 public mempool에 브로드캐스트 하지 않고
// Flashbots MEV-Protect RPC로 직접 전송
// RPC: https://rpc.flashbots.net

📘 Chapter 07

풀스택 DApp 아키텍처 — 프론트엔드부터 배포까지

Next.js + wagmi + viem + RainbowKit으로 현업 수준 DApp 완전 구현

7.1 현업 DApp 기술 스택 (2024-2025)

레이어 기술 역할 대안
프레임워크 Next.js 14 (App Router) SSR/SSG, API Routes Vite + React
Web3 상태관리 wagmi v2 + viem 지갑 연결, 컨트랙트 호출 ethers.js v6
지갑 UI RainbowKit 멀티 지갑 연결 UI ConnectKit, Web3Modal
인덱싱 The Graph (Subgraph) 이벤트 인덱싱, GraphQL API Moralis, Alchemy API
스마트컨트랙트 Hardhat + OpenZeppelin 개발, 테스트, 배포 Foundry
메타데이터 저장 IPFS (Pinata/NFT.Storage) NFT 이미지, JSON 저장 Arweave, Filecoin
// Next.js 14 + wagmi v2 + RainbowKit 풀스택 DApp 예시
// app/providers.tsx

"use client";
import { RainbowKitProvider, getDefaultConfig } from "@rainbow-me/rainbowkit";
import { WagmiProvider } from "wagmi";
import { mainnet, polygon, arbitrum, sepolia } from "wagmi/chains";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "@rainbow-me/rainbowkit/styles.css";

const config = getDefaultConfig({
  appName: "My DApp",
  projectId: "YOUR_WALLETCONNECT_PROJECT_ID", // walletconnect.com에서 발급
  chains: [mainnet, polygon, arbitrum, sepolia],
  ssr: true, // Next.js SSR 지원
});

const queryClient = new QueryClient();

export function Providers({ children }) {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        <RainbowKitProvider>
          {children}
        </RainbowKitProvider>
      </QueryClientProvider>
    </WagmiProvider>
  );
}

// ========================
// app/components/NFTMint.tsx — wagmi v2 컨트랙트 호출
// ========================
"use client";
import { useWriteContract, useReadContract, useAccount, useWaitForTransactionReceipt } from "wagmi";
import { parseEther } from "viem";
import NFT_ABI from "@/abi/MyNFT.json";

const NFT_ADDRESS = "0xYourNFTContractAddress";

export function NFTMintButton() {
  const { address, isConnected } = useAccount();

  // 컨트랙트 읽기 — 현재 공급량
  const { data: totalSupply } = useReadContract({
    address: NFT_ADDRESS,
    abi: NFT_ABI,
    functionName: "totalSupply",
  });

  // 컨트랙트 쓰기 — mint 함수 호출
  const { writeContract, data: hash, isPending, error } = useWriteContract();

  // 트랜잭션 영수증 대기
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });

  const handleMint = () => {
    writeContract({
      address: NFT_ADDRESS,
      abi: NFT_ABI,
      functionName: "mint",
      args: [address],
      value: parseEther("0.01"), // 0.01 ETH 민팅 가격
    });
  };

  return (
    <div>
      <p>총 발행량: {totalSupply?.toString() ?? "0"}</p>
      <button onClick={handleMint} disabled={!isConnected || isPending || isConfirming}>
        {isPending ? "지갑 서명 대기중..." :
         isConfirming ? "체인 확인중..." :
         isSuccess ? "민팅 완료! 🎉" : "NFT 민팅 (0.01 ETH)"}
      </button>
      {error && <p style={{color:"red"}}>오류: {error.message}</p>}
      {isSuccess && <p>트랜잭션: {hash}</p>}
    </div>
  );
}

7.2 The Graph — 이벤트 인덱싱 & GraphQL

# schema.graphql — Subgraph 스키마 정의
type Transfer @entity(immutable: true) {
  id: Bytes!
  from: Bytes!
  to: Bytes!
  tokenId: BigInt!
  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}

type User @entity {
  id: Bytes!           # 지갑 주소
  ownedTokens: [BigInt!]!
  totalMinted: BigInt!
}

# GraphQL 쿼리 예시 (프론트엔드에서 사용)
query {
  transfers(first: 10, orderBy: blockTimestamp, orderDirection: desc) {
    id
    from
    to
    tokenId
    blockTimestamp
  }
  users(where: { totalMinted_gt: "0" }) {
    id
    ownedTokens
    totalMinted
  }
}

🎤 Chapter 08

현업 블록체인 개발자 면접 Q&A TOP 15

실제 면접에서 자주 출제되는 질문과 완벽한 모범 답안

Q1. Uniswap V2와 V3의 핵심 차이점은 무엇인가요?

A: V2는 x*y=k 공식으로 유동성을 [0,∞) 전 구간에 분산하여 자본 효율이 낮습니다. V3는 LP가 원하는 가격 범위[tickLower, tickUpper]에만 집중적으로 유동성을 공급할 수 있어 자본 효율이 최대 4000배 높아집니다. LP 포지션이 NFT(ERC-721)로 표현되며, 수수료 티어도 0.05%/0.3%/1%로 세분화되었습니다.

Q2. 재진입 공격(Reentrancy Attack)을 방어하는 방법 3가지를 설명하세요.

A:CEI 패턴 — Checks(검증) → Effects(상태변경) → Interactions(외부호출) 순서 준수. 상태를 먼저 변경 후 외부 호출. ② ReentrancyGuard — OpenZeppelin의 nonReentrant 수정자로 함수 진입 잠금. ③ Pull Payment 패턴 — 직접 ETH를 보내지 않고 수령자가 직접 출금하는 방식으로 전환.

Q3. Flash Loan은 어떻게 담보 없이 대출이 가능한가요?

A: 블록체인의 원자성(Atomicity)을 활용합니다. 단일 트랜잭션 내에서 대출→사용→상환이 모두 이루어지며, 상환하지 못하면 트랜잭션 전체가 롤백됩니다. 따라서 Aave 입장에서는 자금을 잃을 위험이 없습니다. 수수료는 V3 기준 대출액의 0.05%입니다.

Q4. ERC-721과 ERC-1155의 차이점과 각각의 사용 사례는?

A: ERC-721은 각 NFT가 완전히 고유한 1:1 토큰으로 희귀성이 중요한 아트/컬렉터블에 적합합니다. ERC-1155는 단일 컨트랙트에서 여러 토큰 타입(FT+NFT 혼합)을 관리하며 배치 전송으로 가스를 절약합니다. 게임 아이템(아이템 1000개 + 금화 10000개)처럼 대규모 토큰 관리에 최적입니다.

Q5. Chainlink VRF를 사용하는 이유는? blockhash로 난수를 만들면 안 되나요?

A: blockhash를 이용한 난수는 채굴자가 조작할 수 있습니다(채굴자가 원하지 않는 결과가 나오면 해당 블록을 버릴 수 있음). Chainlink VRF는 오프체인에서 난수를 생성하고 그 증명(proof)을 함께 제출하여 온체인에서 검증합니다. 누구도 결과를 예측하거나 조작할 수 없는 검증 가능한 난수를 보장합니다.

Q6. MEV(Maximal Extractable Value)란 무엇이며, 개발자로서 어떻게 대응하나요?

A: MEV는 블록 생성자가 트랜잭션 순서 조작으로 추가 수익을 추출하는 행위입니다. 프런트런닝, 샌드위치 공격, 청산 봇 경쟁 등이 있습니다. 대응책: ① slippage와 deadline 파라미터로 최악의 경우 보호 ② Commit-Reveal 패턴으로 거래 내용 숨김 ③ Flashbots MEV-Protect RPC로 private mempool 사용 ④ 배치 경매 방식 도입.

Q7. DAO에서 Timelock Controller가 필요한 이유는?

A: 악의적인 프로포절이 빠르게 통과되어 프로토콜을 탈취하는 것을 방지합니다. 예를 들어 "Treasury의 모든 자금을 공격자에게 전송"하는 프로포절이 통과되더라도, TimelockController의 2일 대기 기간 동안 커뮤니티가 발견하고 취소할 수 있습니다. 보안과 거버넌스의 핵심 장치입니다.

Q8. proxy 패턴(Upgradeable Contract)을 설명하고 주요 종류를 비교하세요.

A: 스마트 컨트랙트는 배포 후 수정 불가이므로, Proxy 패턴으로 로직을 분리합니다. 사용자는 항상 Proxy에 접근하고, Proxy는 delegatecall로 Implementation(로직) 컨트랙트에 실행을 위임합니다. 주요 종류: ① Transparent Proxy — admin과 일반 사용자가 다른 컨트랙트에 접근, 함수 선택자 충돌 방지. ② UUPS — 업그레이드 로직이 Implementation에 있어 가스 효율적. ③ Beacon Proxy — 여러 Proxy가 하나의 Beacon을 참조, 일괄 업그레이드 가능.

Q9. Aave에서 Health Factor가 1 미만이 되면 어떤 일이 발생하나요?

A: 청산(Liquidation)이 발생합니다. 청산 봇이 해당 포지션의 부채를 최대 50%(CLOSE_FACTOR)까지 대신 상환하고, 담보를 청산 보너스(5~15%)를 적용한 할인가에 가져갑니다. 이 인센티브가 청산자에게 수익을 제공하여 프로토콜의 지급 능력을 유지합니다. HF = (담보 × 청산임계값) / 총부채 공식으로 계산됩니다.

Q10. ERC20Votes에서 delegate()를 꼭 호출해야 하는 이유는?

A: ERC20Votes는 과거 블록의 투표권 스냅샷을 추적합니다. 토큰을 보유해도 delegate()로 투표권을 활성화하지 않으면 투표에 참여할 수 없습니다. 자기 자신에게 delegate(address(self)) 하거나 다른 주소에 위임할 수 있습니다. 이는 토큰 전송 시점에 대한 투표 조작을 방지하기 위한 설계입니다.

Q11. 스마트 컨트랙트에서 오라클 조작 공격(Oracle Manipulation)이란?

A: 단일 DEX의 spot price를 오라클로 사용할 때, 공격자가 Flash Loan으로 일시적으로 가격을 조작하는 공격입니다. 예: Flash Loan → DEX에서 대량 매수 → 가격 급등 → 담보 가치 부풀려 대출 → 원상복구. 방어: Chainlink Price Feed 같은 탈중앙화 오라클 사용, TWAP(시간 가중 평균 가격) 활용.

Q12. wagmi와 ethers.js의 차이점과 선택 기준은?

A: ethers.js는 범용 Ethereum 라이브러리로 Node.js/백엔드에서도 사용 가능합니다. wagmi는 React 전용 hooks 라이브러리로, viem을 내부적으로 사용하며 지갑 상태 관리, 계정 연결, 캐싱(TanStack Query)을 React 생태계에 최적화하여 제공합니다. 프론트엔드 DApp에는 wagmi, 스크립트/백엔드에는 ethers.js/viem을 권장합니다.

Q13. IPFS와 Arweave의 차이점은? NFT 메타데이터 저장에 어느 것이 적합한가?

A: IPFS는 분산 파일 시스템으로 콘텐츠 주소(CID)를 사용하지만 영구 저장이 보장되지 않습니다(Pinning 서비스 필요). Arweave는 일회성 요금으로 영구 저장을 보장하는 블록체인 기반 스토리지입니다. 고가치 NFT 컬렉션은 Arweave, 일반적인 경우 Pinata/NFT.Storage를 통한 IPFS를 사용합니다.

Q14. Foundry와 Hardhat의 차이점과 각각의 장단점은?

A: Hardhat은 JavaScript/TypeScript 기반으로 플러그인 생태계가 풍부하고 학습 자료가 많습니다. Foundry는 Rust 기반으로 컴파일/테스트 속도가 월등히 빠르며 Solidity로 테스트를 작성합니다(별도 언어 불필요). Fuzz testing, invariant testing이 내장되어 있어 보안 감사 시 선호됩니다. 현업에서는 Hardhat 배포 + Foundry 테스트를 함께 사용하기도 합니다.

Q15. 가스 최적화 기법 5가지를 설명하세요.

A:storage → memory/calldata: SLOAD(800gas) 대신 MLOAD(3gas) 활용, 루프 내 캐싱. ② uint256 패킹: 동일 슬롯(32바이트)에 여러 작은 uint 묶기. ③ custom error: require("string") 대신 error MyError()로 가스 절약. ④ unchecked 블록: 오버플로우 불가능한 연산에 unchecked{} 사용(Solidity 0.8+ 자동 체크 비용 절감). ⑤ event 대신 calldata 최소화: 굳이 온체인에 저장 불필요한 데이터는 event로 대체.

✅ NFT·DeFi 심화 학습 체크리스트

🔨 스마트 컨트랙트

  • ERC-1155 멀티토큰 구현
  • NFT 마켓플레이스 (수수료+오퍼)
  • Uniswap V3 유동성 공급
  • Flash Loan 실전 구현
  • DAO 거버넌스 전체 배포
  • Chainlink VRF 연동

🛡️ 보안 & 최적화

  • MEV 방어 패턴 적용
  • 슬리피지 & deadline 보호
  • 오라클 신선도 검증
  • 가스 최적화 5가지 기법
  • CEI 패턴 & ReentrancyGuard
  • Health Factor 모니터링

🌐 프론트엔드

  • wagmi v2 + RainbowKit 셋업
  • useWriteContract 활용
  • 이벤트 리스닝 구현
  • The Graph Subgraph 작성
  • IPFS 메타데이터 업로드
  • 트랜잭션 상태 UX 처리

📋 면접 & 취업

  • 면접 Q&A TOP 15 암기
  • GitHub 포트폴리오 3개 이상
  • Etherscan 배포 기록 확보
  • 감사 보고서 1개 작성 경험
  • 테스트 커버리지 95% 이상
  • 기술 블로그 포스팅

🎉 BlockchainDevGuide0005 완료!

ERC-1155, NFT 마켓플레이스, Uniswap V3, Aave Flash Loan, DAO 거버넌스,
Chainlink 오라클, MEV 방어, 풀스택 DApp까지 현업 핵심 스킬을 완전 정복했습니다!

📖 다음 편 예고

BlockchainDevGuide0006
스마트 컨트랙트 보안·감사 완전 정복
재진입 공격 · 오라클 조작 · Slither · Echidna · 버그 바운티

반응형