Back to Blog

Revest Finance 취약점: 재진입 공격 그 이상

Code Auditing
March 31, 2022
7 min read

2022년 3월 27일, 이더리움의 스테이킹 DeFi 프로젝트 Revest Finance가 ERC-1155 콜백 메커니즘으로 인해 공격을 받았으며, 이로 인해 약 200만 달러 상당의 토큰(BLOCKS, ECO, LYXe, RENA)이 탈취되었습니다. 우리는 해당 공격을 먼저 분석하였고, 그날 밤(UTC+8) 분석 결과를 트위터에 게시하였습니다.

사실, 트위터를 작성하던 당시 우리는 Revest TokenVault 컨트랙트의 특정 함수에 대해 여전히 몇 가지 의문을 가지고 있었습니다. 우리는 해당 컨트랙트의 기능을 이해하기 위해 면밀히 살펴보았고, 이후 이것이 또 다른 치명적인 제로데이 취약점임을 발견하였습니다. 이 취약점은 훨씬 더 단순한 방법으로 악용될 수 있으며, 동일한 막대한 손실을 초래할 수 있습니다(실제로 발생한 공격과 같이).

이후 우리는 즉시 Revest Finance 팀에 연락하였고, 팀은 신속히 응답하여 해당 취약점에 대한 임시 해결책을 제안하였습니다. 취약점이 더 이상 트리거될 수 없음을 확인한 후, 이 블로그를 공개하기로 결정하였습니다.

이 블로그의 이후 내용은 세 가지 부분으로 구성됩니다: Revest Finance의 메커니즘, 기존의 재진입 공격, 그리고 새로운 제로데이 취약점.

Revest Finance FNFT란 무엇인가

Revest Finance의 금융 대체불가능토큰(FNFT)은 잠긴 자산에 대한 미래 권리의 신뢰 없는 이전을 가능하게 합니다. 엔트리 컨트랙트(Revest 컨트랙트)는 기초 자산을 잠금으로써 FNFT를 발행하는 세 가지 인터페이스를 제공합니다:

  • mintTimeLock: 일정 기간이 지난 후 기초 자산의 잠금이 해제됩니다.
  • mintValueLock: 기초 자산의 가치가 지정된 값 이상으로 오르거나 이하로 떨어질 때 잠금이 해제됩니다.
  • mintAddressLock: 지정된 계정에 의해 기초 자산의 잠금이 해제됩니다.

Revest 컨트랙트는 기초 자산을 잠그고 잠금 해제하기 위해 다른 세 개의 컨트랙트와 연결됩니다.

  • FNFTHandler: ERC-1155 토큰에서 상속됩니다. 모든 잠금에 대해 증가하는 fnftId를 가진 새로운 FNFT를 생성합니다. 잠금은 생성 시 새 FNFT의 총 공급량을 규정합니다. FNFT는 다른 방법으로는 발행될 수 없으며, 기초 자산 잠금 해제를 위해 소각될 수 있습니다.

  • LockManager: 생성 시 각 잠금의 잠금 해제 조건을 기록하고, 잠금 해제 시 잠금을 해제할 수 있는지 여부를 결정합니다.

  • TokenVault: 기초 자산을 수신 및 전송하고, 지정된 FNFT의 가치와 같은 각 FNFT의 메타데이터를 기록합니다.

mintAddressLock을 예로 들어 FNFT 발행 과정을 설명하겠습니다.

그림 1
그림 2

위 두 그림은 FNFT가 어떻게 생성, 발행 및 소각되는지를 설명합니다. 구체적으로, 사용자 A는 100 WETH를 Revest Finance에 잠금으로써 fnftId가 1인 해당 FNFT를 생성합니다. 최종적으로, 지정된 수신자에게 지정된 지분에 따라 100개의 1-FNFT를 발행합니다.

잠긴 기초 자산이 잠금 해제되면, 모든 1-FNFT는 1(*1e18) WETH를 받기 위해 소각될 수 있습니다. 그림 2에서 볼 수 있듯이, 사용자 B는 25개의 1-FNFT를 소각하여 25(* 1e18) WETH를 출금합니다.

또한, Revest 컨트랙트는 depositAdditionalToFNFT라는 또 다른 인터페이스를 제공하며, 이는 이후에 논의될 두 가지 취약점을 유발합니다.

먼저 다음 두 그림을 사용하여 이 함수의 일반적인 사용 방법을 설명하겠습니다.

그림 3
그림 4

depositAdditionalToFNFT 함수는 기존 잠금(fnftId로 지정)에 더 많은 기초 자산을 잠급니다. 합리적으로(그림 3), 이 함수는 지정된 수량이 지정된 FNFT의 총 공급량과 동일할 것을 요구하고, 추가된 자산을 각 지정된 FNFT에 균등하게 분배합니다.

그렇지 않은 경우(그림 4), 최신 fnftId로 새로운 잠금을 생성하고, 지정된 수량의 이전 FNFT를 소각하며 지정된 수량의 새 FNFT를 발행하고, 다음 코드에서 볼 수 있듯이 새 잠금의 depositAmount를 이전 잠금의 depositAmount와 지정된 금액의 합계로 기록합니다.

// 이제 토큰 볼트로 전송합니다
if(fnft.asset != address(0)){
    IERC20(fnft.asset).safeTransferFrom(_msgSender(), vault, quantity * amount);
}

ITokenVault(vault).handleMultipleDeposits(fnftId, newFNFTId, fnft.depositAmount + amount);

emit FNFTAddionalDeposited(_msgSender(), newFNFTId, quantity, amount);

TokenVault 컨트랙트에 기록된 depositAmount는 지정된 FNFT 하나가 출금할 수 있는 기초 자산의 양을 나타내므로, 해당 작업은 지정된 수량의 이전 FNFT 가치를 이전 잠금에서 새 잠금으로 이전합니다.

(지정된 수량이 총 공급량보다 크면 트랜잭션이 되돌려집니다)

재진입 취약점이란 무엇인가

이 부분에서는 재진입 공격이 어떻게 작동하는지 설명하고 근본 원인과 수정 방법을 논의하겠습니다.

그림 5
그림 6
그림 7

위 세 그림은 재진입 공격의 전체 과정을 기본적으로 설명합니다. 구체적으로, 공격자는 먼저 아무 가치가 없는 2개의 1-FNFT를 발행하기 위해 0 RENA 토큰을 잠급니다. 그 다음, 공격자는 다시 0 RENA 토큰을 잠그지만, 역시 (현재) 아무 가치가 없는 360,000개의 2-FNFT를 발행합니다. 마지막 단계에서, 공격자는 ERC-1155 토큰 표준에서 상속된 FNFTHandler의 콜백 메커니즘을 통해 Revest 컨트랙트의 depositAdditionalToFNFT 함수에 재진입하여, fnftId 업데이트 이전에 fnftId가 2인 잠금의 depositAmount를 덮어씁니다. 결과적으로, 공격자는 depositAmount가 1e18인 360,001개의 2-FNFT를 얻게 되며, 이는 TokenVault 컨트랙트에서 360,001 * 1e18 RENA를 출금할 수 있음을 의미합니다. 또한, 유일한 비용은 1e18 RENA입니다.

수정 방법

Revest Finance의 코드는 전형적인 재진입 패턴과 완전히 일치합니다: fnftId 사용 -> 콜백 메커니즘이 있는 외부 호출 -> fnftId 업데이트. 따라서, 가장 직접적인 수정 방법은 이 패턴을 깨는 것입니다. 수정된 코드는 다음과 같습니다:

function mint(
    address account, 
    uint id, 
    uint amount, 
    bytes memory data
) external override onlyRevestController {
    require(amount > 0, "Invalid amount");
    require(supply[id] == 0, "Repeated mint for the same FNFT");
    supply[id] += amount;
    fnftsCreated += 1;
    _mint(account, id, amount, data);
}

첫째, 업데이트 작업을 외부 호출(_mint) 이전으로 이동하여 공격을 방지할 수 있습니다. 둘째, 시스템이 0개의 FNFT 발행 및 동일한 FNFT의 반복 발행을 허용하지 않으므로, 시스템이 예상대로 작동하는지 확인하기 위한 두 가지 검사를 추가하여 시스템의 안전성을 향상시킬 수 있습니다.

새로운 제로데이 취약점

Revest Finance의 코드를 분석하는 과정에서, TokenVault 컨트랙트의 handleMultipleDeposits 함수가 항상 우리를 혼란스럽게 했으며, 해당 코드는 다음과 같습니다.

function handleMultipleDeposits(
    uint fnftId,
    uint newFNFTId,
    uint amount
) external override onlyRevestController {
    require(amount >= fnfts[fnftId].depositAmount, 'E003');
    IRevest.FNFTConfig storage config = fnfts[fnftId];
    config.depositAmount = amount;
    mapFNFTToToken(fnftId, config);
    if(newFNFTId != 0) {
        mapFNFTToToken(newFNFTId, config);
    }
}

depositAdditionalToFNFT 함수 호출 중에, handleMultipleDeposits 함수는 이전 잠금의 depositAmount를 변경하거나 새 잠금의 depositAmount를 기록합니다. newFNFTId가 0인 경우, 이는 기존 잠금에 추가 자산을 추가하는 작업이므로 새 잠금의 depositAmount를 기록하지 않습니다.

상식적으로, newFNFTId가 0이 아닌 경우, 새 잠금의 depositAmount만 기록하고 이전 잠금의 depositAmount는 변경하지 않아야 합니다. 그러나 코드를 보면, 새 잠금의 depositAmount를 기록할 뿐만 아니라 이전 잠금의 depositAmount도 변경하고 있습니다.

우리는 이것이 심각한 제로데이 로직 취약점이라고 판단하여 이를 검증하기 위한 PoC를 작성하였습니다. 다음 세 그림은 PoC가 어떻게 작동하는지를 설명합니다.

그림 8
그림 9
그림 10

구체적으로, 공격자는 먼저 360,000개의 1-FNFT를 발행하기 위해 0 RENA를 잠급니다. 그 후, 공격자는 depositAdditionalToFNFT 함수를 직접 호출하여 새로운 잠금을 생성합니다. 로직 버그로 인해, TokenVault 컨트랙트는 이전 잠금의 depositAmount를 0에서 1e18로 잘못 변경합니다. 결과적으로, 공격자는 359,999 RENA에 해당하는 359,999개의 1-FNFT를 얻게 됩니다. 명백히, 이 PoC는 실제 재진입 공격보다 훨씬 더 단순합니다.

취약점 수정을 위한 임시 해결책

이것은 로직 버그이며, 다음 코드를 사용하여 수정할 것을 권장합니다.

function handleMultipleDeposits(
    uint fnftId,
    uint newFNFTId,
    uint amount
) external override onlyRevestController {
    require(amount >= fnfts[fnftId].depositAmount, 'E003');
    IRevest.FNFTConfig memory config = fnfts[fnftId];
    config.depositAmount = amount;
    if(newFNFTId != 0) {
        mapFNFTToToken(newFNFTId, config);
    } else {
        mapFNFTToToken(fnftId, config);
    }
}

취약한 두 컨트랙트인 TokenVaultFNFTHandler는 많은 중요한 상태를 저장하고 있으므로, 프로젝트는 상태를 마이그레이션하지 않고는 TokenVault 컨트랙트와 FNFTHandler 컨트랙트를 재배포할 수 없습니다. 이 취약점에 대한 추가 공격을 방지하기 위해, 프로젝트는 잠재적인 공격자에게 노출되는 공격 표면을 줄이기 위해 더 복잡한 기능을 비활성화한 라이트 버전의 Revest 컨트랙트를 재배포하였습니다. 임시 해결책을 확인한 후, 우리는 라이트 Revest 컨트랙트가 이 블로그에서 언급된 가능한 공격을 완화할 수 있다고 판단합니다.

교훈

DeFi 프로젝트를 안전하게 만드는 것은 쉬운 일이 아닙니다. 코드 감사 외에도, 우리는 커뮤니티가 프로젝트 상태를 모니터링하는 사전 예방적 방법을 취하고, 공격이 발생하기 전에 차단해야 한다고 생각합니다.

BlockSec 소개

BlockSec은 2021년 세계적으로 著名한 보안 전문가 그룹에 의해 설립된 선구적인 블록체인 보안 회사입니다. 이 회사는 대중적 채택을 촉진하기 위해 새롭게 부상하는 Web3 세계의 보안성과 사용성을 향상시키는 데 전념하고 있습니다. 이를 위해 BlockSec은 스마트 컨트랙트 및 EVM 체인 보안 감사 서비스, 보안 개발 및 위협을 사전에 차단하기 위한 Phalcon 플랫폼, 자금 추적 및 조사를 위한 MetaSleuth 플랫폼, 그리고 크립토 세계에서 Web3 빌더들이 효율적으로 서핑할 수 있도록 하는 MetaDock 익스텐션을 제공합니다.

현재까지 이 회사는 MetaMask, Uniswap Foundation, Compound, Forta, PancakeSwap 등 300개 이상의 저명한 고객사에 서비스를 제공하였으며, Matrix Partners, Vitalbridge Capital, Fenbushi Capital을 포함한 저명한 투자자들로부터 두 차례의 투자 라운드에서 수천만 달러를 유치하였습니다.

공식 웹사이트: https://blocksec.com/

공식 트위터 계정: https://twitter.com/BlockSecTeam

Sign up for the latest updates
~$410만 손실: Taiko, SecondFi 익스플로잇 | BlockSec 위클리
Security Insights

~$410만 손실: Taiko, SecondFi 익스플로잇 | BlockSec 위클리

이 주간 블록체인 보안 리포트는 2026년 6월 22~28일 발생한 주요 사건 2건을 다루며, 이더리움과 카르다노에서 약 410만 달러의 피해가 확인됐습니다. Taiko 브릿지 공격은 노출된 SGX 서명 키와 디버그 엔클레이브를 거부하지 못한 증명 정책 결함을 이용해 악성 증명자를 등록하고 L2 상태 증명을 위조했습니다. SecondFi 지갑은 Ed25519 논스 도출 시 비밀 입력이 제거되는 결함으로 공개 트랜잭션 데이터만으로 개인 키 복구가 가능했습니다.

~$18M 손실: jaredFromSubway, Aztec 등 | BlockSec 위클리
Security Insights

~$18M 손실: jaredFromSubway, Aztec 등 | BlockSec 위클리

이 주간 블록체인 보안 보고서는 2026년 6월 15일~21일을 다루며, 이더리움과 BNB 체인에서 3건의 주요 사고가 발생해 약 $18.3M의 손실이 발생했습니다. jaredFromSubway 사건은 MEV 봇이 차익거래를 위해 신뢰할 수 없는 제3자 컨트랙트에 자산을 승인한 역방향 승인 공격으로, 가짜 래퍼 토큰과 스왑 풀을 이용해 약 $15M 손실이 발생했습니다. Aztec은 이스케이프 해치 ZK 회로의 제약 누락으로 공격자가 가짜 머클 트리로 온체인 검증을 통과했습니다.

Web3 컴패니언: 오픈소스 보안 에이전틱 지갑

Web3 컴패니언: 오픈소스 보안 에이전틱 지갑

BlockSec가 Web3 Companion을 오픈소스로 공개했습니다. 이 보안 중심의 에이전트 지갑은 자체 AI 에이전트를 신뢰하지 않는 방식으로 설계되었으며, 키 격리, 강력한 정책, Passkey를 활용해 온체인 자산을 보호합니다.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit