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. 게임 아이템 컨트랙트 완전 구현
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 패턴 + 로열티
2-3. 더치 경매(Dutch Auction) 구현
더치 경매란? 높은 가격에서 시작해 시간이 지날수록 낮아지는 방식. 구매자는 원하는 가격이 되는 순간 구매. NFT 민팅의 공정한 가격 발견에 자주 사용됩니다.
📘 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 · 버그 바운티