스마트 컨트랙트 보안: 재진입·오라클·가스 그리핑 대응

스마트 컨트랙트의 재진입 공격, 오라클 신뢰 경계, 가스 그리핑 위험을 설계·검증·운영 관점에서 다룬다.

2026-08-14 · 최초 발행 2025-10-31

외부 호출, 데이터 입력, 가스 비용이 만나는 지점

스마트 컨트랙트는 블록체인 위에서 코드와 상태를 자동으로 실행하는 디지털 계약이다. 불변성, 투명성, 자동화라는 장점이 있지만 배포 뒤 수정이 어렵고, 설계 단계에서 남은 취약점은 그대로 공격 표면이 된다.

2016년 The DAO 해킹에서는 재진입(Reentrancy) 취약점이 악용돼 약 360만 ETH가 탈취됐다. 외부 호출을 먼저 수행하고 상태를 나중에 갱신한 설계, 그리고 테스트와 감사의 미흡이 근본 원인이었다.

보안 범위는 재진입에만 머물지 않는다. 온체인 계약이 오프체인 데이터를 받아들이는 오라클에는 신뢰성·무결성·가용성 문제가 걸려 있다. 메타트랜잭션, 릴레이, 번들러 구조에서는 제3자가 가스 비용을 부담하는 모델 자체가 가스 도난(Gas Theft)과 그리핑(Griefing)의 표적이 될 수 있다.

취약점은 서로 다른 신뢰 경계에서 시작된다

재진입은 외부 호출과 상태 업데이트의 순서가 뒤집힐 때 발생한다. Checks-Effects-Interactions를 지키지 않거나 pull-payment를 적용하지 않으면 공격자가 제어하는 함수가 연쇄 호출될 여지가 생긴다.

오라클은 데이터 경계가 문제다. 중앙화된 오라클, 적은 수의 피드, 비동기 지연은 가격이나 상태를 왜곡할 수 있다. 데이터 소스의 다양성과 합의·집계 로직이 없으면 시세 조작, 지연·중단, 이상치 주입 위험이 커진다.

ERC-4337 또는 메타TX 모델에서는 Paymaster와 Relayer가 비용을 부담한다. 트랜잭션이 revert되더라도 사용된 가스는 소모되므로, 의도적으로 실패를 유도해 비용을 떠넘기는 그리핑 표면이 생긴다.

재진입은 상태를 바꾸기 전에 외부로 제어권을 넘길 때 발생한다

악성 컨트랙트취약 컨트랙트사용자악성 컨트랙트취약 컨트랙트사용자fallback()에서 재진입 시도상태 갱신 이전 반복 전송 발생withdraw(amount)call{value: amount} (외부 전송)withdraw(amount) 재호출call{value: amount} 반복잔액 과다 인출 후 상태 갱신

방어의 중심은 상태 선갱신(CEI), 뮤텍스(ReentrancyGuard), pull-payment 패턴, 최소 권한 설계다. 외부 호출을 줄이고, 호출이 실패해도 안전한 흐름을 설계해야 한다.

검증 가능한 코드로 방어를 고정하기

사용 환경은 Solidity ^0.8.20, OpenZeppelin Contracts ^4.9의 ReentrancyGuard와 SafeERC20, Hardhat 또는 Foundry를 전제로 한다. 메인넷 포크 테스트도 권장한다.

상태 갱신보다 앞선 외부 호출

다음 예시는 의도적으로 단순화한 취약한 형태다. 잔액을 0으로 만들기 전에 외부 호출을 수행한다.

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

contract VulnerableVault {
    mapping(address => uint256) public balances;

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

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "no balance");
        // 외부 호출 먼저
        (bool ok,) = msg.sender.call{value: amount}("");
        require(ok, "transfer failed");
        // 상태 갱신이 뒤에 있어 재진입 가능
        balances[msg.sender] = 0;
    }
}

수정본은 CEI와 ReentrancyGuard, pull-payment를 함께 사용한다. 사용자의 청구 가능 금액을 먼저 기록하고, 외부 호출은 claim()의 마지막에 둔다.

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

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

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

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

    function requestWithdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount, "insufficient");
        // 1) 상태 선갱신
        balances[msg.sender] -= amount;
        // 2) pull-payment: 사용자가 나중에 청구
        pending[msg.sender] += amount;
    }

    mapping(address => uint256) public pending;

    function claim() external nonReentrant {
        uint256 amount = pending[msg.sender];
        require(amount > 0, "nothing");
        pending[msg.sender] = 0;
        // 외부 호출은 마지막
        (bool ok,) = msg.sender.call{value: amount}("");
        require(ok, "transfer failed");
    }
}

테스트에서는 재진입 시도 컨트랙트가 claim을 재호출할 때 revert되는지 확인한다. sum(balances)+sum(pending)=contract balance라는 잔액 보존 불변식도 함께 검증한다.

오라클 값은 신선도와 편차를 함께 확인한다

오라클은 다중 소스, 중앙값·가중 평균·TWAP, 하트비트, 신선도(staleness)와 편차(deviation) 가드, 페일오버와 회로차단기(circuit breaker)를 조합해 다룬다.

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

interface AggregatorV3Interface {
    function latestRoundData() external view returns (
        uint80 roundId,int256 answer,uint256 startedAt,uint256 updatedAt,uint80 answeredInRound
    );
    function decimals() external view returns (uint8);
}

contract PriceGuard {
    AggregatorV3Interface public immutable feed;
    uint256 public constant MAX_STALENESS = 60; // 초
    uint256 public constant MAX_DEVIATION_BPS = 500; // 5%

    int256 public lastPrice;

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

    function guardedPrice() external returns (int256) {
        (, int256 price,, uint256 updatedAt,) = feed.latestRoundData();
        require(price > 0, "bad price");
        require(block.timestamp - updatedAt <= MAX_STALENESS, "stale");

        if (lastPrice > 0) {
            // 절대 편차 비율 계산
            uint256 lp = uint256(lastPrice > 0 ? lastPrice : 1);
            uint256 cp = uint256(price);
            uint256 diff = lp > cp ? lp - cp : cp - lp;
            require(diff * 10000 / lp <= MAX_DEVIATION_BPS, "too volatile");
        }

        lastPrice = price;
        return price;
    }
}

다중 피드를 median 또는 TWAP으로 집계하고, Heartbeat 미수신 시 회로차단기를 작동시켜 대체 오라클 경로로 페일오버할 수 있다. 온체인 가스비가 급등하는 구간에서는 업링크 지연을 고려한 캐시 전략도 필요하다.

Paymaster에는 비용을 제한하는 정책이 필요하다

가스 리스크는 사전 시뮬레이션, 상한 가스 제한, 화이트리스트와 쿼터, Paymaster 정책 엔진, 커밋-리빌과 예치금(slashing)을 통한 남용 억제로 통제한다. 최대 가스·수수료 캡과 revert 사유 블랙리스트도 정책에 포함할 수 있다.

// Pseudocode for Paymaster policy
function validateUserOp(UserOperation op) external returns (bool) {
    require(whitelist[op.sender], "not whitelisted");
    require(op.callGasLimit <= MAX_CALL_GAS, "gas too high");
    // 사전 시뮬레이션
    bool ok = simulate(op);
    if (!ok) return false; // revert 대신 비용 최소화 정책
    // 예치금 충분성 확인
    require(stake[op.sender] >= MIN_STAKE, "low stake");
    return true;
}

실패 빈도와 가스 소모 상한을 계정별로 모니터링해 자동 임시 차단에 활용할 수 있다. 번들러와 릴레이어가 경쟁하는 환경에서는 commit-reveal이 MEV와 그리핑을 함께 완화하는 수단이 된다.

EIP-3529 이후 가스 환급 정책이 변화해 가스 토큰 전략의 유효성은 낮아졌다. 최신 사양과 가스 비용 모델은 주기적으로 점검해야 한다.

프로토콜 성격에 따른 적용 지점

DEX와 렌딩 프로토콜은 TWAP, 지연값, 중앙값 집계로 시세 조작 저항성을 높일 수 있다. 급격한 편차가 감지되면 대출 한도 축소와 청산 유예 트리거를 적용한다.

NFT와 분산 마켓플레이스에서는 재진입 방지와 pull-payment가 로열티 정산을 보호한다. 오라클로 외부 결제 환율을 반영한다면 회로차단기를 포함한다.

지갑과 계정 추상화(ERC-4337) 서비스에서는 Paymaster 정책 엔진으로 가스 스폰서 비용을 제한하고, 사전 시뮬레이션과 리스크 점수화로 남용 계정을 자동 차단할 수 있다. 트레저리와 DAO 거버넌스는 타임락, 다중서명, 업그레이드 프록시로 비상 대응 경로를 확보하며 온체인 감시 알람과 버그바운티 프로그램을 병행한다.

검증과 런타임 대응을 하나의 운영선으로 묶기

설계 단계에서는 신뢰 경계를 정의하고 외부 호출을 최소화하며, 권한과 업그레이드 경로를 명세한다. 오라클의 소스·집계·가용성 정책도 문서화 대상이다.

개발과 검증 단계에서는 정적 분석(Slint, Slither), 형태추론·포멀 스펙, Foundry 퍼즈와 프로퍼티 테스트, 메인넷 포크 시뮬레이션을 병행한다. 포멀 검증에서는 Spec과 Invariant를 다루고, Differential 또는 Shadow 메인넷 시뮬레이션도 검증 라인에 포함한다.

배포 뒤에는 감사지적사항 0, 커버리지와 불변식 검증 통과를 릴리즈 게이트로 둔다. 가격 편차와 가스 실패율을 대시보드에서 관측하고 자동 서킷브레이커를 연결한다. 타임락·다중서명, 긴급 스위치, 공지 템플릿은 인시던트 대응 경로로 준비한다. 필요하면 긴급 업그레이드·키 회수와 사용자 보호 펀드도 운용한다.

다중 검증 파이프라인 도입은 크리티컬 취약점 유출 확률을 6080% 감소시키는 가정 기반 추정이다. 오라클 다중화와 가드를 적용하면 가격 조작 취약 트랜잭션 실패율을 70% 이상 사전 차단할 수 있고, Paymaster를 정책화하면 실패 트랜잭션당 평균 가스 소모를 3050% 절감할 수 있다. 사용자 신뢰와 TVL 변동성, 인시던트 대응 시간, 커뮤니티 리스크 커뮤니케이션, 출시 주기의 안정성도 함께 다뤄진다.

방어 방식별 운영상 차이

전략 성능 확장성 일관성 안정성 운영 편의
CEI + ReentrancyGuard 높음 높음 높음 높음 높음
Pull-Payment 패턴 중간 높음 높음 높음 중간
다중 오라클 + TWAP 중간 중간 높음 높음 중간
회로차단기/페일오버 중간 높음 중간 높음 중간
Paymaster 정책 엔진 중간 높음 높음 중간 중간
스마트 컨트랙트블록체인 보안재진입 공격오라클ERC-4337