Guider/Blockchain/BlockchainDevGuide0006
Blockchain#06

BlockchainDevGuide0006

스마트 컨트랙트 보안·감사 완전 정복

✦ BLOCKCHAIN DEV GUIDE SERIES ✦

스마트 컨트랙트 보안·감사
완전 정복 A-Z

재진입 공격 · 오라클 조작 · 접근 제어 · Slither · Echidna · 버그 바운티

Guide0006 학습 기간: 3-4주 난이도: 고급

🛡️ 이 가이드를 완료하면?

  • 재진입 공격 패턴 5가지 완전 이해
  • 오라클 조작 공격 방어 구현
  • 정수 오버플로우/언더플로우 방어
  • 접근 제어 취약점 완전 차단
  • Slither 정적 분석 도구 활용
  • Echidna 퍼징 테스트 작성
  • Foundry 불변량 테스트 구현
  • 감사 보고서 작성 실습
  • 버그 바운티 참여 전략
  • 현업 보안 체크리스트 완성

📊 시리즈 진행 현황

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

📚 전체 학습 목차

챕터 주제 핵심 기술 난이도
01 재진입 공격 완전 분석 CEI 패턴, ReentrancyGuard, Pull Payment ★★★★☆
02 오라클 조작 공격 TWAP, Chainlink, Spot Price 위험 ★★★★☆
03 정수 오버플로우·언더플로우 SafeMath, Solidity 0.8+, unchecked ★★★☆☆
04 접근 제어 취약점 Ownable, RBAC, tx.origin 위험 ★★★☆☆
05 플래시 론 공격 패턴 가격 조작, 거버넌스 공격, 방어 패턴 ★★★★★
06 Slither 정적 분석 자동 취약점 감지, 커스텀 디텍터 ★★★☆☆
07 Echidna 퍼징 테스트 속성 기반 테스트, 불변량 정의 ★★★★☆
08 Foundry 불변량 테스트 invariant test, handler, ghost variables ★★★★★
09 감사 보고서 작성 심각도 분류, PoC 작성, 리포트 구조 ★★★★☆
10 면접 Q&A TOP 15 보안 면접 완벽 대비 ★★★★☆

📘 Chapter 01

재진입 공격(Reentrancy Attack) — 스마트 컨트랙트 최대 위협

2016년 DAO 해킹 360만 ETH 탈취 사례부터 최신 공격 패턴과 완벽 방어까지

🚨 역대 최대 보안 사고 — 2016 DAO 해킹

2016년 6월, The DAO 컨트랙트에서 재진입 공격으로 360만 ETH(당시 약 6,000만 달러) 탈취. 이 사고로 이더리움이 ETH/ETC로 하드포크됨. 공격자는 withdraw() 함수가 잔액을 업데이트하기 전에 반복 호출하여 잔액보다 훨씬 많은 ETH를 인출.

1.1 재진입 공격 동작 원리

// ❌ 취약한 컨트랙트 — 재진입 공격에 노출
contract VulnerableBank {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    // 취약점: ETH 전송 후에 잔액을 0으로 업데이트 → 재진입 가능
    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        
        // ⚠️ 위험: 외부 호출 먼저 (ETH 전송)
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
        
        // ⚠️ 상태 업데이트가 외부 호출 이후 → 재진입 공격 허용
        balances[msg.sender] = 0;
    }
}

// ❌ 공격자 컨트랙트
contract Attacker {
    VulnerableBank public bank;
    uint256 public attackAmount = 1 ether;

    constructor(address _bank) {
        bank = VulnerableBank(_bank);
    }

    function attack() external payable {
        require(msg.value >= attackAmount, "Need ETH");
        bank.deposit{value: attackAmount}();
        bank.withdraw(); // 첫 withdraw 호출
    }

    // ETH 수신 시 자동 실행되는 fallback
    receive() external payable {
        // bank.balances[this] 가 아직 0이 안 됐으므로 재호출 가능!
        if (address(bank).balance >= attackAmount) {
            bank.withdraw(); // 재진입!!! 반복 탈취
        }
    }
}

// ✅ 방어 1: CEI (Checks-Effects-Interactions) 패턴
contract SafeBank_CEI {
    mapping(address => uint256) public balances;

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        
        // ✅ Effects 먼저 — 상태 변경
        balances[msg.sender] = 0;
        
        // ✅ Interactions 나중에 — 외부 호출
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

// ✅ 방어 2: ReentrancyGuard (뮤텍스 잠금)
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SafeBank_Guard is ReentrancyGuard {
    mapping(address => uint256) public balances;

    // nonReentrant: 함수 실행 중 재진입 완전 차단
    function withdraw() external nonReentrant {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        balances[msg.sender] = 0;
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

// ✅ 방어 3: Pull Payment 패턴 (가장 안전)
import "@openzeppelin/contracts/security/PullPayment.sol";

contract SafeBank_Pull is PullPayment {
    function refund(address payee, uint256 amount) internal {
        // ETH를 직접 보내지 않고 escrow에 보관
        _asyncTransfer(payee, amount);
    }
    // 수령자가 직접 withdrawPayments() 호출하여 출금
}

1.2 크로스 함수 & 크로스 컨트랙트 재진입

⚠️ 고급 재진입 패턴 — 단순 nonReentrant로 막을 수 없는 경우

  • 크로스 함수 재진입: withdraw()와 transfer() 두 함수 모두를 이용 (하나에만 nonReentrant 걸면 뚫림)
  • 읽기 전용 재진입: view 함수를 통해 업데이트 전 상태를 읽어 다른 프로토콜을 속이는 공격
  • ERC-777 재진입: ERC-20과 달리 전송 시 콜백이 발생 → 토큰 전송 시 재진입 가능
// ⚠️ 크로스 함수 재진입 예시
contract CrossFunctionVulnerable {
    mapping(address => uint256) public balances;

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        // ETH 전송 (여기서 공격자의 receive()가 실행됨)
        payable(msg.sender).transfer(amount);
        balances[msg.sender] = 0; // 아직 0이 아님!
    }

    // 공격자는 receive()에서 transfer()를 호출 → balances 복사 가능
    function transfer(address to, uint256 amount) external {
        require(balances[msg.sender] >= amount); // 아직 잔액이 있음!
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}

// ✅ 해결: 모든 관련 함수에 nonReentrant 적용 OR 전역 잠금 사용
contract CrossFunctionSafe is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function withdraw() external nonReentrant {
        uint256 amount = balances[msg.sender];
        balances[msg.sender] = 0; // CEI 패턴 적용
        payable(msg.sender).transfer(amount);
    }

    function transfer(address to, uint256 amount) external nonReentrant {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}

📘 Chapter 02

오라클 조작 공격 — DeFi 최대 위협 완전 분석

Flash Loan + Spot Price 조작으로 수억 달러 탈취 — 원리와 완벽 방어까지

💡 오라클 조작 공격의 핵심 원리

온체인 DEX의 spot price(현재 가격)를 오라클로 사용하면, 플래시 론으로 일시적으로 가격을 조작할 수 있습니다. 예: 1. 플래시 론으로 대량 USDC 대출 → 2. Uniswap에서 대량 매수로 ETH 가격 인위적 상승 → 3. 부풀려진 ETH를 담보로 프로토콜에서 과대 대출 → 4. 자산 인출 → 5. ETH 매도로 가격 원상복구 → 6. 플래시 론 상환. 단 1개 트랜잭션으로 완료.

2.1 실제 공격 사례와 피해액

프로토콜 연도 피해액 공격 방법
Harvest Finance 2020.10 $34M USDC/USDT Curve spot price 조작
Compound 2020.11 $89M DAI Uniswap V2 가격 조작 → 청산 공격
Cream Finance 2021.10 $130M yUSD 오라클 조작 + 플래시 론
Mango Markets 2022.10 $117M MNGO 토큰 가격 조작 후 과대 대출
// ❌ 취약한 오라클 — Uniswap spot price 직접 사용
contract VulnerableOracle {
    IUniswapV2Pair public pair; // WETH/USDC 쌍

    // 현재 블록의 spot price만 읽음 → 조작 가능
    function getPrice() external view returns (uint256) {
        (uint112 reserve0, uint112 reserve1, ) = pair.getReserves();
        // token0=WETH, token1=USDC
        return (uint256(reserve1) * 1e18) / uint256(reserve0); // USDC per WETH
    }
}

// ✅ 방어 1: Chainlink Price Feed (가장 권장)
contract ChainlinkOracle {
    AggregatorV3Interface immutable priceFeed;

    constructor(address _feed) { priceFeed = AggregatorV3Interface(_feed); }

    function getPrice() external view returns (uint256) {
        (, int256 price, , uint256 updatedAt, ) = priceFeed.latestRoundData();
        require(block.timestamp - updatedAt < 1 hours, "STALE"); // 신선도 검증
        require(price > 0, "INVALID");
        return uint256(price); // 8 decimals
    }
}

// ✅ 방어 2: TWAP (시간 가중 평균 가격) — Uniswap V3
contract UniswapTWAP {
    IUniswapV3Pool public pool;
    uint32 public twapInterval = 1800; // 30분 평균

    function getTWAP() external view returns (uint256) {
        uint32[] memory secondsAgos = new uint32[](2);
        secondsAgos[0] = twapInterval; // 30분 전
        secondsAgos[1] = 0;            // 현재

        (int56[] memory tickCumulatives, ) = pool.observe(secondsAgos);
        
        int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
        int24 avgTick = int24(tickCumulativesDelta / int56(uint56(twapInterval)));
        
        // tick → price 변환
        uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(avgTick);
        return FullMath.mulDiv(uint256(sqrtPriceX96) ** 2, 1e18, 2**192);
    }
}

// ✅ 방어 3: 다중 오라클 + 편차 검증
contract MultiOracle {
    AggregatorV3Interface immutable chainlink;
    uint256 constant MAX_DEVIATION = 5; // 5% 이상 편차 시 거래 거부

    function getVerifiedPrice() external view returns (uint256) {
        uint256 chainlinkPrice = _getChainlinkPrice();
        uint256 twapPrice      = _getTWAP();
        
        // 두 오라클 가격 편차 검증
        uint256 deviation = chainlinkPrice > twapPrice
            ? ((chainlinkPrice - twapPrice) * 100) / chainlinkPrice
            : ((twapPrice - chainlinkPrice) * 100) / twapPrice;
        
        require(deviation <= MAX_DEVIATION, "ORACLE_MANIPULATION_DETECTED");
        return (chainlinkPrice + twapPrice) / 2; // 평균값 사용
    }
}

📘 Chapter 03

정수 오버플로우·언더플로우 & 접근 제어 취약점

수학적 함정과 권한 관리 실수로 인한 취약점 완전 분석

// 정수 오버플로우 — Solidity 0.8.0 이전의 주요 취약점
// Solidity 0.8.0+는 기본적으로 오버플로우 시 revert

// ❌ 취약: Solidity ^0.7.x
contract VulnerableToken {
    mapping(address => uint256) balances;

    function transfer(address to, uint256 amount) external {
        // uint256 underflow: balances[msg.sender]가 amount보다 작으면
        // 0 - 1 = 2^256-1 (엄청난 양!)
        balances[msg.sender] -= amount; // ❌ 언더플로우 가능
        balances[to] += amount;
    }
}

// ✅ 방어 1: Solidity 0.8+ 사용 (자동 체크)
// ✅ 방어 2: SafeMath (0.8 이전 버전)
// ✅ 방어 3: unchecked는 오버플로우 불가능한 경우에만 사용
contract SafeArithmetic {
    // 가스 최적화: for 루프에서 unchecked 사용 (i < arr.length 보장됨)
    function sumArray(uint256[] calldata arr) external pure returns (uint256 total) {
        uint256 len = arr.length;
        for (uint256 i = 0; i < len; ) {
            total += arr[i];
            unchecked { ++i; } // overflow 불가 (len < 2^256)
        }
    }
}

// ===== 접근 제어 취약점 =====

// ❌ tx.origin 사용 (피싱 공격 가능)
contract TxOriginVulnerable {
    address public owner;

    function transferOwnership(address newOwner) external {
        // tx.origin은 트랜잭션 최초 발신자
        // 중간 컨트랙트 경유 시 조작 가능
        require(tx.origin == owner, "NOT_OWNER"); // ❌ 위험!
        owner = newOwner;
    }
}
// 공격 시나리오: owner가 악성 컨트랙트를 실행 →
// 악성 컨트랙트가 transferOwnership 호출 → tx.origin = owner 통과!

// ✅ msg.sender 사용
contract SafeAccessControl {
    address public owner;

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

    // RBAC (역할 기반 접근 제어) — OpenZeppelin AccessControl
    // DEFAULT_ADMIN_ROLE, MINTER_ROLE, PAUSER_ROLE 등 세분화
}

// ❌ 초기화 취약점 (Uninitialized Proxy Storage)
contract UninitializedVulnerable {
    address public owner;
    bool public initialized;

    function initialize() external {
        // 초기화 여부 체크 없음 → 누구나 initialize() 재호출 가능
        owner = msg.sender; // ❌ 공격자가 owner 탈취 가능
    }
}

// ✅ initializer 수정자 (OpenZeppelin Initializable)
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract SafeInitialize is Initializable {
    address public owner;

    function initialize() external initializer {
        // initializer 수정자: 단 1회만 실행 가능
        owner = msg.sender; // ✅ 안전
    }
}

📘 Chapter 04

플래시 론 공격 패턴 심화 — 거버넌스 공격 & 방어

단 1 트랜잭션으로 DAO를 탈취하는 거버넌스 공격과 방어 메커니즘

// 플래시 론 거버넌스 공격 시나리오:
// 1. Aave에서 대량 거버넌스 토큰 플래시 론
// 2. 자기 자신에게 delegate() → 대량 투표권 획득
// 3. 악성 프로포절 즉시 통과 (정족수 초과)
// 4. Treasury 자금 전액 인출 실행
// 5. 거버넌스 토큰 상환 → 모든 것이 1 트랜잭션

// ✅ 방어 1: 스냅샷 기반 투표 (ERC20Votes)
// ERC20Votes는 프로포절 생성 시점의 블록 스냅샷 사용
// 플래시 론으로 토큰을 빌려도 해당 블록 이전 잔액이 0이면 투표권 없음

// ✅ 방어 2: Timelock (2일 이상 대기)
// 거버넌스 공격이 성공해도 실행 전 커뮤니티가 발견 가능

// ✅ 방어 3: Quorum 상향 + Voting Period 연장
// 최소 정족수를 높게 설정, 투표 기간을 길게 (최소 1주일)

// ✅ 방어 4: 고가치 작업에 추가 Timelock
function executeHighValueAction(uint256 amount) external {
    require(amount <= dailyLimit, "EXCEEDS_DAILY_LIMIT");
    require(block.timestamp >= lastExecution + 1 days, "TOO_FREQUENT");
    // ...
}

📘 Chapter 05

Slither — 자동 취약점 정적 분석 도구 완전 활용

Trail of Bits의 Slither로 스마트 컨트랙트 취약점을 자동으로 탐지한다

5.1 Slither 설치 및 기본 사용법

# Slither 설치
pip3 install slither-analyzer

# 기본 분석 실행
slither contracts/MyToken.sol

# 특정 탐지기만 실행
slither contracts/ --detect reentrancy-eth,reentrancy-no-eth

# JSON 리포트 출력
slither contracts/ --json slither-report.json

# 상세 정보 출력
slither contracts/ --print call-graph
slither contracts/ --print cfg
slither contracts/ --print inheritance-graph

# Hardhat 프로젝트에서 실행
slither . --hardhat-ignore-compile

5.2 Slither 주요 탐지기 분류

심각도 탐지기 설명
High reentrancy-eth ETH 전송 재진입 취약점
High suicidal 누구나 selfdestruct 호출 가능
Medium tx-origin tx.origin 인증 사용
Medium unused-return 함수 반환값 미사용
Low events-maths 중요 연산 후 이벤트 미발생
Informational solc-version 권장 solc 버전 사용 여부
# 커스텀 Slither 탐지기 작성 (Python)
# detectors/custom_reentrancy.py

from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.core.cfg.node import NodeType

class CustomReentrancyDetector(AbstractDetector):
    ARGUMENT = "custom-reentrancy"
    HELP = "Custom reentrancy detector"
    IMPACT = DetectorClassification.HIGH
    CONFIDENCE = DetectorClassification.MEDIUM

    WIKI = "https://example.com/wiki"
    WIKI_TITLE = "Custom Reentrancy"
    WIKI_DESCRIPTION = "Detects potential reentrancy"
    WIKI_EXPLOIT_SCENARIO = "..."
    WIKI_RECOMMENDATION = "Use CEI pattern or ReentrancyGuard"

    def _detect(self):
        results = []
        for contract in self.contracts:
            for function in contract.functions:
                # ETH 전송 후 storage 쓰기 패턴 탐지
                if self._has_eth_transfer_before_storage(function):
                    result = self.generate_result([
                        "Potential reentrancy in ", function, "
"
                    ])
                    results.append(result)
        return results

📘 Chapter 06

Echidna — 퍼징 테스트로 숨겨진 버그 발견

Trail of Bits의 Echidna 스마트 컨트랙트 퍼저로 불변량(invariant)을 자동 검증한다

🔍 Echidna란?

Echidna는 속성 기반 퍼징(Property-Based Fuzzing) 도구입니다. 개발자가 "항상 참이어야 하는 조건(불변량)"을 Solidity로 작성하면, Echidna가 수천~수백만 개의 무작위 트랜잭션을 생성하여 그 불변량을 깨뜨리는 입력값을 찾아냅니다.

// ERC-20 토큰 Echidna 퍼징 테스트
// echidna_test.sol

pragma solidity ^0.8.19;
import "./MyToken.sol";

contract EchidnaTest is MyToken {
    address internal constant USER1 = address(0x10000);
    address internal constant USER2 = address(0x20000);

    constructor() {
        // 초기 상태 설정
        _mint(USER1, 1000 * 10**18);
        _mint(USER2, 500  * 10**18);
    }

    // ===== 불변량 1: 총 공급량은 항상 민팅 합계와 같아야 한다 =====
    function echidna_total_supply_constant() public view returns (bool) {
        return totalSupply() == 1500 * 10**18;
    }

    // ===== 불변량 2: 잔액 합 <= 총 공급량 =====
    function echidna_balance_sum() public view returns (bool) {
        return balanceOf(USER1) + balanceOf(USER2) <= totalSupply();
    }

    // ===== 불변량 3: 잔액은 절대 음수가 될 수 없다 (uint이므로 당연) =====
    function echidna_no_negative_balance() public view returns (bool) {
        return balanceOf(msg.sender) >= 0; // uint256은 항상 true
    }

    // ===== 불변량 4: approve 후 allowance는 정확해야 한다 =====
    function echidna_allowance_after_approve(address spender, uint256 amount) 
        public returns (bool) 
    {
        approve(spender, amount);
        return allowance(msg.sender, spender) == amount;
    }

    // ===== 불변량 5: transfer 후 잔액 보존 =====
    function echidna_transfer_balance_preservation(address to, uint256 amount)
        public returns (bool)
    {
        uint256 senderBefore   = balanceOf(msg.sender);
        uint256 receiverBefore = balanceOf(to);
        
        if (senderBefore >= amount && to != msg.sender) {
            transfer(to, amount);
            return balanceOf(msg.sender) == senderBefore - amount
                && balanceOf(to) == receiverBefore + amount;
        }
        return true;
    }
}

# Echidna 실행
# echidna-test echidna_test.sol --contract EchidnaTest
# echidna-test echidna_test.sol --contract EchidnaTest --config echidna.yaml
# echidna.yaml 설정 파일
testLimit: 50000       # 최대 테스트 횟수
seqLen: 100           # 시퀀스당 최대 트랜잭션 수
timeout: 300          # 제한 시간 (초)
workers: 4            # 병렬 워커 수

# 테스트 모드
testMode: "property"  # property | assertion | exploration

# 커버리지 출력
coverage: true

# 가스 최적화
gasEnabled: false     # 가스 계산 비활성화로 속도 향상

# corpusDir: "./corpus" # 이전 실행 결과 재사용

📘 Chapter 07

Foundry 불변량 테스트 — 고급 보안 테스트의 정수

Handler 패턴, Ghost Variables, 불변량 정의로 완벽한 보안 테스트 구현

// Foundry 불변량(Invariant) 테스트 — 현업 수준
// test/invariant/InvariantBank.t.sol

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

import "forge-std/Test.sol";
import "forge-std/InvariantTest.sol";
import "../../src/Bank.sol";

// Handler: Foundry가 호출할 수 있는 함수들을 정의
// Ghost Variables: 테스트에서 추적하는 가상의 상태 변수
contract BankHandler is Test {
    Bank public bank;

    // Ghost variables — 우리가 직접 추적하는 상태
    uint256 public ghost_depositSum;   // 모든 예금 합계
    uint256 public ghost_withdrawSum;  // 모든 출금 합계

    address[] public actors;
    address internal currentActor;

    constructor(Bank _bank) {
        bank = _bank;
        // 테스트 액터 설정
        actors.push(makeAddr("user1"));
        actors.push(makeAddr("user2"));
        actors.push(makeAddr("user3"));
    }

    modifier useActor(uint256 actorSeed) {
        currentActor = actors[actorSeed % actors.length];
        vm.startPrank(currentActor);
        _;
        vm.stopPrank();
    }

    // Foundry가 무작위로 호출할 함수들
    function deposit(uint256 actorSeed, uint256 amount) external useActor(actorSeed) {
        amount = bound(amount, 0.01 ether, 100 ether); // 범위 제한
        deal(currentActor, amount);
        
        bank.deposit{value: amount}();
        ghost_depositSum += amount;
    }

    function withdraw(uint256 actorSeed, uint256 amount) external useActor(actorSeed) {
        uint256 balance = bank.balances(currentActor);
        if (balance == 0) return;
        
        amount = bound(amount, 0, balance);
        bank.withdraw(amount);
        ghost_withdrawSum += amount;
    }
}

// 불변량 테스트 컨트랙트
contract InvariantBank is Test, InvariantTest {
    Bank bank;
    BankHandler handler;

    function setUp() external {
        bank    = new Bank();
        handler = new BankHandler(bank);

        // Foundry에게 handler의 함수만 호출하도록 지정
        targetContract(address(handler));
    }

    // ===== 불변량 1: ETH 잔액 = 총 예금 - 총 출금 =====
    function invariant_bankBalance() external {
        assertEq(
            address(bank).balance,
            handler.ghost_depositSum() - handler.ghost_withdrawSum(),
            "Bank balance mismatch"
        );
    }

    // ===== 불변량 2: 개별 잔액 합 = 컨트랙트 ETH 잔액 =====
    function invariant_solvency() external {
        uint256 sumOfBalances;
        address[] memory actors = handler.getActors();
        for (uint256 i = 0; i < actors.length; i++) {
            sumOfBalances += bank.balances(actors[i]);
        }
        assertEq(address(bank).balance, sumOfBalances, "Solvency broken");
    }

    // 실행: forge test --match-contract InvariantBank -v
    // 출력: runs: 256, μ: 127341, ~: 129043
}

📘 Chapter 08

스마트 컨트랙트 감사 보고서 작성 실습

Code4rena, Sherlock, ImmuneFi 스타일의 전문 감사 보고서 작성법

8.1 감사 보고서 구조 & 심각도 분류

심각도 정의 예시 버그 바운티 보상
Critical 자금 직접 탈취 가능 재진입, 접근 제어 완전 우회 $50,000~$10M+
High 큰 자금 손실 가능 오라클 조작, 청산 실패 $10,000~$50,000
Medium 조건부 자금 손실 슬리피지 미보호, 잘못된 수식 $1,000~$10,000
Low 소액 또는 간접 영향 이벤트 누락, 부정확한 계산 $100~$1,000
Gas/Info 개선 권장사항 가스 최적화, 코드 품질 인정 or 소액
# 감사 보고서 템플릿 (Code4rena / Sherlock 스타일)

## [H-01] ReentrancyGuard 미적용으로 인한 ETH 탈취 가능

**심각도:** High  
**컨트랙트:** Bank.sol  
**함수:** withdraw()  

### 취약점 설명
withdraw() 함수에서 ETH 전송 후 잔액 업데이트 순서가 잘못되어 재진입 공격에 취약합니다.

### 공격 경로 (PoC)
1. 공격자가 1 ETH를 Bank에 예금
2. withdraw() 호출
3. ETH 수신 시 receive() 함수에서 withdraw() 재호출
4. bank.balance가 소진될 때까지 반복
5. 공격자는 1 ETH 예금으로 전체 ETH 탈취

### PoC 코드
```solidity
contract AttackTest is Test {
    Bank bank;
    Attacker attacker;

    function setUp() public {
        bank = new Bank();
        // 다른 사용자 예금 설정
        vm.deal(address(bank), 10 ether);
        attacker = new Attacker(address(bank));
    }

    function test_reentrancyAttack() public {
        vm.deal(address(attacker), 1 ether);
        uint256 bankBalanceBefore = address(bank).balance;
        
        attacker.attack{value: 1 ether}();
        
        // 공격자가 bank 전체 자금 탈취
        assertEq(address(bank).balance, 0);
        assertGt(address(attacker).balance, bankBalanceBefore);
    }
}
```

### 권장 수정사항
CEI 패턴 적용 및 ReentrancyGuard 추가:
```solidity
function withdraw() external nonReentrant {
    uint256 amount = balances[msg.sender];
    require(amount > 0);
    balances[msg.sender] = 0; // Effects 먼저
    (bool ok,) = msg.sender.call{value: amount}(""); // Interaction 나중
    require(ok);
}
```

8.2 버그 바운티 참여 전략

🥉 ImmuneFi

  • DeFi 최대 버그 바운티
  • 최고 $10M 보상
  • Uniswap, Aave 등 참여
  • immunefi.com

🏆 Code4rena

  • 경쟁형 감사 플랫폼
  • 4일~2주 감사 콘테스트
  • 순위별 보상 지급
  • code4rena.com

🛡️ Sherlock

  • 감사자+보험 결합
  • USDC 보상 지급
  • 시니어 감사자 우대
  • sherlock.xyz

🎤 Chapter 09

블록체인 보안 전문가 면접 Q&A TOP 15

보안 감사 회사 & DeFi 프로토콜 면접에서 자주 출제되는 핵심 질문

Q1. 재진입 공격의 3가지 방어 방법과 각각의 장단점은?

A: ① CEI 패턴: 구현 간단, 개발자 실수 가능. ② ReentrancyGuard: 확실한 보호, 가스 약간 증가(~20 gas). ③ Pull Payment: 가장 안전하나 UX 복잡. 실무에서는 CEI + nonReentrant 조합 권장. 크로스 함수 재진입은 모든 관련 함수에 nonReentrant 필요.

Q2. Slither와 Mythril의 차이점은?

A: Slither는 정적 분석(AST 분석)으로 빠르고 오탐이 적으며 커스텀 탐지기 작성 가능. Mythril은 심볼릭 실행(모든 실행 경로 탐색)으로 더 정확하지만 느리고 복잡한 컨트랙트에서 타임아웃 발생. 실무: Slither를 CI/CD에 통합하고 Mythril은 심층 분석 시 사용.

Q3. 퍼징 테스트(Fuzzing)와 단위 테스트의 차이점은?

A: 단위 테스트는 개발자가 예상하는 시나리오를 직접 작성. 퍼징은 불변량(항상 참이어야 할 조건)만 정의하면 도구가 수십만 개의 무작위 입력으로 자동 검증. 개발자가 예상하지 못한 엣지 케이스를 발견하는 데 탁월. 현업에서는 둘 다 필수.

Q4. TWAP 오라클은 모든 오라클 조작 공격을 방어할 수 있나요?

A: TWAP은 장기간 가격 조작을 어렵게 하지만 완벽하지 않습니다. 짧은 TWAP 기간(예: 5분)은 여전히 조작 가능. 저유동성 쌍은 긴 TWAP도 조작 가능. 권장: ① Chainlink Price Feed 우선 사용 ② TWAP은 30분 이상으로 설정 ③ 두 오라클 가격 편차 5% 이상 시 거래 거부.

Q5. 프록시 패턴의 storage collision 문제를 설명하고 해결 방법은?

A: Proxy와 Implementation이 같은 storage slot을 사용하면 덮어쓰기 발생. 예: Proxy의 _implementation이 slot 0이고 Implementation도 변수가 slot 0이면 충돌. 해결: ① EIP-1967 표준 — 랜덤 해시로 슬롯 지정 (예: keccak256("eip1967.proxy.implementation") - 1) ② 최신 OZ TransparentUpgradeableProxy는 자동 처리.

Q6. selfdestruct의 보안 위험과 현재 상태는?

A: selfdestruct는 컨트랙트를 파괴하고 ETH를 강제 전송(receive() 없이도 전송). 위험: ① 컨트랙트 ETH 잔액을 receive()를 우회해 강제 전송 → 잔액 기반 로직 오작동 ② contract existence 체크 우회. 현재: EIP-6780(Dencun 업그레이드)으로 selfdestruct는 동일 트랜잭션 내 생성된 컨트랙트에서만 동작하도록 제한됨.

Q7. Foundry 불변량 테스트에서 Handler 패턴을 사용하는 이유는?

A: Handler 없이 Foundry가 컨트랙트를 직접 퍼징하면 대부분의 호출이 revert되어 의미 있는 상태에 도달하지 못합니다. Handler는 유효한 입력 범위를 bound()로 제한하고, 올바른 순서(예: 출금 전 예금)를 보장하며, Ghost Variables로 외부 상태를 추적합니다. 이로 인해 퍼징 효율이 수십~수백 배 향상됩니다.

Q8. ERC-4626 Vault의 주요 보안 취약점은?

A:인플레이션 공격(Inflation Attack): 첫 번째 예금자가 1 wei를 예금하고 대량의 자산을 직접 전송하여 shares 가치를 인위적으로 부풀려 후속 예금자가 0 shares를 받게 함. 방어: virtual offset 사용(OZ ERC4626 기본 적용). ② 가격 조작: totalAssets()가 조작 가능한 값을 참조할 때.

Q9. block.timestamp 사용 시 주의사항은?

A: block.timestamp는 검증자가 약 ±12초(1 slot) 범위에서 조작 가능합니다. 짧은 시간 기반 로직(예: 10초 잠금)은 취약. 반면 1분 이상의 시간 비교는 실용적으로 안전. 로또나 난수 시드로 절대 사용 금지. block.number 사용도 블록타임 가정 시 주의 필요.

Q10. delegatecall의 보안 위험을 설명하세요.

A: delegatecall은 호출된 코드가 호출자의 storage, msg.sender, msg.value 컨텍스트에서 실행됩니다. 위험: ① 악성 Implementation 컨트랙트가 Proxy의 storage를 마음대로 변경 ② storage collision으로 _owner 덮어쓰기 ③ selfdestruct 호출 시 Proxy 파괴. 방어: 신뢰할 수 있는 Implementation만 사용, EIP-1967 표준 슬롯 사용.

Q11. Code4rena에서 좋은 점수를 받기 위한 전략은?

A: ① 중복 제출 최소화 — 다른 감사자가 찾을 명백한 버그보다 독창적인 취약점 발굴. ② PoC 코드 필수 — 실제 익스플로잇 가능성 증명. ③ 심각도 근거 명확히 — 왜 High인지 자금 손실 경로 서술. ④ 코드 커버리지 높이기 — 비즈니스 로직의 엣지 케이스 집중 분석. ⑤ 상태 전환 다이어그램 그리기 — 예상치 못한 상태 발견.

Q12. 프론트런닝 방지를 위한 Commit-Reveal 패턴의 한계는?

A: ① 2단계 트랜잭션 필요 → UX 저하 ② Commit 이후 Reveal 기간 동안 시장 변화로 불리해져도 커밋 이행 강제됨 ③ Reveal 단계에서 다른 참여자 값을 보고 참여 취소 가능(Last-Revealer 문제). 대안: Flashbots private mempool, SUAVE(MEV-Share), EIP-7702 등 최신 솔루션 활용.

Q13. ERC-20 approve/transferFrom의 프런트런닝 문제와 해결책은?

A: approve(100) → 지출자가 100 사용 → approve(200)으로 변경 시, 지출자가 프런트런닝으로 100을 먼저 전송 후 변경된 200도 사용 → 총 300 탈취 가능. 해결: ① increaseAllowance/decreaseAllowance 사용 ② 먼저 0으로 재설정 후 새 값으로 설정 ③ ERC-2612 permit() 사용(서명 기반, 별도 approve 트랜잭션 불필요).

Q14. 스마트 컨트랙트 업그레이드 시 발생할 수 있는 보안 문제는?

A: ① Storage 레이아웃 변경 — 기존 storage 슬롯 순서 변경 시 데이터 손상. 해결: 항상 새 변수를 끝에 추가. ② 초기화 재공격 — 새 Implementation의 initialize()를 공격자가 먼저 호출. 해결: _disableInitializers() 생성자에 추가. ③ 신뢰 가정 붕괴 — 업그레이드 권한을 가진 주소가 악의적 코드 배포. 해결: Timelock + 다중서명으로 업그레이드 제어.

Q15. 현업 보안 감사 프로세스(풀 감사 기준)를 설명하세요.

A:스코핑(1일): 감사 범위, 시간, 비용 협의. ② 자동화 분석(1일): Slither, Mythril, Semgrep 실행. ③ 수동 코드 리뷰(3~5일): 비즈니스 로직 분석, 공격 경로 탐색. ④ PoC 작성(1~2일): 발견된 취약점 익스플로잇 증명. ⑤ 리포트 작성(1일): 심각도 분류, 수정 권고. ⑥ 재감사(1~2일): 수정사항 검증. 총 2~4주 소요.

✅ 스마트 컨트랙트 보안 학습 체크리스트

🔥 공격 패턴 이해

  • 재진입 공격 PoC 작성
  • 크로스 함수 재진입 이해
  • 플래시 론 오라클 조작
  • 거버넌스 공격 시나리오
  • ERC-20 approve 프런트런
  • 정수 오버/언더플로우

🛡️ 방어 패턴 구현

  • CEI 패턴 + nonReentrant
  • Chainlink 오라클 연동
  • TWAP 오라클 구현
  • AccessControl RBAC 구현
  • Commit-Reveal 구현
  • Timelock 거버넌스 배포

🔧 도구 활용

  • Slither CI/CD 통합
  • Echidna 퍼징 테스트 작성
  • Foundry 불변량 테스트
  • Handler 패턴 구현
  • Mythril 심층 분석
  • Semgrep 규칙 작성

📋 커리어

  • 감사 보고서 1개 작성
  • Code4rena 콘테스트 참여
  • ImmuneFi 등록
  • GitHub 보안 포트폴리오
  • 면접 Q&A TOP 15 암기
  • 역대 해킹 사례 분석 블로그

🎉 BlockchainDevGuide0006 완료!

재진입 공격, 오라클 조작, 플래시 론 공격, Slither, Echidna, Foundry 불변량 테스트,
감사 보고서 작성, 버그 바운티까지 — 보안 전문가로 가는 완벽한 로드맵을 완주했습니다!

📖 마지막 편 예고

BlockchainDevGuide0007
실전 프로젝트 완전 정복 A-Z
DeFi·NFT·DAO 풀스택 완성 + 포트폴리오 + 취업 전략

반응형