Back to Blog

Autheo Network·UNCX 해킹으로 약 387만 USD 손실 | BlockSec 주간 소식

Code Auditing
2026년 10월 22일
8 min read
Key Insights
  • 이 보고서는 주목할 만한 보안 사고 3건을 다루며, 총 추정 손실액은 약 387만 USD입니다. 사고 유형은 개인 키 탈취, 베스팅 회계 오류, 취약한 지갑 시드 생성이었습니다.

  • UNCX Network의 Smart Token Vesting V2 사고는 Ethereum, BNB Chain, Base, Robinhood Chain 전반에 걸쳐 10개 자산에 영향을 미쳤습니다. 결함이 있는 총지분 계산에서 토큰 대 지분 비율이 서로 다른 시점에 생성된 베스팅 트랜치가 혼합되어, 실현되었거나 다른 경로로 이동한 가치 약 80.7만 USD를 빼낼 수 있었습니다.

  • 통합 회계에서는 각 트랜치의 토큰 수량, 지분 산정 기준, 베스팅 진행 상태 간의 관계를 유지해야 합니다. 이를 하나의 비율로 합치면 한 포지션이 공동 잔액에서 보유분을 초과해 인출할 수 있어, 다른 포지션의 청구를 충족하는 데 사용할 수 있는 자산이 줄어들 수 있습니다.

지난주(2026년 10월 12일–2026년 10월 18일)에는 다음과 같은 주요 보안 사고 3건이 발생했으며, 추정 손실액 합계는 약 387만 USD입니다.

날짜 사고 유형 추정 손실액
2026년 10월 12일 Autheo Network 개인 키 유출 ~176만 USD
2026년 10월 14일 UNCX Network 회계 처리 오류 ~80.7만 USD
2026년 10월 18일 Qubic Wallet 취약한 시드 생성 ~130만 USD

손실액은 실현 가치를 기준으로 추정한 수치입니다.

  • UNCX Network: 잘못된 총합 지분 계산이 이번 주 가장 명확한 컨트랙트 회계 처리 교훈을 보여주므로 이 사고를 자세히 분석합니다.

무료 보안 스캔

자체 자동 분석 엔진으로 신속하게 보안 상태를 점검합니다.

무료로 스캔하기

이번 주 주요 사고: UNCX Network

2026년 10월 14일, 공격자들은 Ethereum, BNB Chain, Base, Robinhood Chain에 배포된 UNCX Network의 Smart Token Vesting V2 컨트랙트를 악용했습니다. 이 사고는 확인된 자산 유출 트랜잭션 11건에서 자산 10종에 영향을 미쳤으며, 실현되었거나 다른 경로로 이동한 가치가 약 80.7609만 USD에 달했습니다 [1]. 근본 원인은 서로 다른 토큰 대 지분 비율로 생성된 베스팅 트랜치를 뒤섞은 잘못된 총합 지분 계산이었습니다. 이로 인해 포지션별 기록과 전체 지분 공급량이 불일치했고, 공격자들이 통제하는 포지션에서 공격자들이 공급한 양보다 많은 토큰을 인출할 수 있었습니다.

배경

UNCX Network는 멀티체인 DeFi 인프라 제공업체입니다. Smart Token Vesting V2 제품을 통해 프로젝트는 ERC-20 토큰을 시간 기반 베스팅 일정에 따라 잠글 수 있습니다. 각 일정은 베스팅 ID로 식별되며 토큰, 생성자, 수익자, 총수량, 이미 지급된 수량, 하나 이상의 잠금 해제 트랜치를 기록합니다. 추가 예치가 가능한 소프트 포지션은 생성 후에도 트랜치를 추가할 수 있으며, 생성자는 베스팅이 완료되지 않은 부분을 취소할 수 있습니다.

같은 토큰의 포지션들은 VestingManager에 있는 하나의 잔액을 공유하며, 프로토콜은 지분을 사용해 각 포지션이 해당 잔액 중 얼마를 보유하는지 결정합니다. 따라서 회계 처리에는 서로 연관된 두 계층이 있습니다.

Global pool, per token
  create / topUp: receive tokens and mint shares
  release: convert releasable shares into tokens
  _totalTokenShares[token]: active shares across all positions

Position record, per vesting ID
  _depositedShares[id]: share basis assigned to the position
  totalAmount: token amount represented by its tranches
  released: shares already paid to the beneficiary

지급 시 매니저는 먼저 해당 일정에서 베스팅이 완료된 양을 계산합니다. _depositedShares[vestingId]을 사용해 totalAmount에서 그 비율에 해당하는 양을 베스팅 완료 지분으로 환산한 다음, released를 빼고 그 결과를 releasableShares라고 합니다. 아래 공식에서 tokenBalance는 해당 일정의 토큰에 대해 매니저가 현재 보유한 ERC-20 잔액으로, 그 토큰의 모든 포지션이 공유합니다. _totalTokenShares[token]은 이에 대응하는 활성 지분의 전체 공급량입니다. 지급되는 토큰 수량은 둘의 비율에 따라 달라집니다.

amount = releasableShares * tokenBalance / _totalTokenShares[token]

베스팅 함수를 통한 예치는 토큰 잔액과 지분 공급량을 모두 늘립니다. 매니저로 ERC-20 토큰을 직접 전송하면 토큰 잔액만 바뀌므로 각 지분이 나타내는 토큰 수량이 달라집니다. 이 지분 모델이 공동 풀의 포지션을 지원하려면 모든 생애주기 작업에서 포지션 기록과 전체 공급량이 일치해야 합니다.

취약점 분석

영향을 받은 Ethereum의 VestingManager 컨트랙트(0xA98F...99F5)에는 release(), topUpVesting(), cancel()에 걸쳐 악용할 수 있는 잘못된 총합 지분 계산이 있습니다. 다른 세 네트워크에도 동일한 영향을 받는 배포가 존재합니다.

아래 분석에서는 상태 변화를 비교하기 쉽도록 D, R, V을 약어로 사용합니다. 이 기호들은 컨트랙트 코드에는 나타나지 않습니다. 표에는 각 기호에 대응하는 컨트랙트 변수 또는 표현식이 나와 있습니다. 대응되는 값은 두 가지 수량 유형으로 나뉩니다.

기호 컨트랙트 변수 / 표현식 수량 유형 의미
D _depositedShares[vestingId] 지분 수량 해당 포지션에 기록된 모든 예치에 배정된 누적 지분 기준값
R schedule.released 지분 수량 수익자에게 이미 지급된 누적 지분
- schedule.totalAmount 토큰 수량 해당 포지션의 현재 트랜치 전체에 걸쳐 기록된 총 토큰 수량
- vestedAmount 토큰 수량 현재 시점에 해당 트랜치에 따라 베스팅이 완료된 totalAmount의 부분
V vestedShare 지분 수량 하나로 혼합된 베스팅 완료 토큰 비율을 D에 적용해 계산한 누적 베스팅 완료 지분

release()에서 컨트랙트는 releasableShares를 계산하고, schedule.released를 늘린 뒤, _totalTokenShares[schedule.token]에서 같은 수량을 뺍니다. _depositedShares[vestingId]은 줄이지 않습니다. 따라서 지급 후에는 저장된 기준값이 여전히 D이지만, 해당 포지션이 전체 공급량에 기여하는 실제 활성 지분은 D - R뿐입니다.

function release(uint256 vestingId) external nonReentrant {
    ...
    uint256 releasableShares = _getReleasableShares(vestingId);
    uint256 balance = IERC20(schedule.token).balanceOf(address(this));
    uint256 amount =
        (releasableShares * balance) / _totalTokenShares[schedule.token];

    schedule.released += releasableShares;
    _totalTokenShares[schedule.token] -= releasableShares;
    IERC20(schedule.token).safeTransfer(msg.sender, amount);
}

cancel()는 포지션의 총합 지분 기준값에 하나로 혼합된 베스팅 비율을 적용합니다. 먼저 getVestedAmount(vestingId)을 통해 vestedAmount를 계산한 다음, 토큰 단위로 표시된 베스팅 완료 비율을 V = (vestedAmount * D) / totalAmount의 방식으로 지분으로 환산합니다. 마지막으로 이 함수는 D - V을 해당 포지션에서 제거할 수 있는 미베스팅 지분으로 취급합니다. V는 누적값이지, 이전 지급 후 남아 있는 베스팅 완료 지분이 아닙니다.

function cancel(uint256 vestingId) external nonReentrant {
    ...
    uint256 vestedAmount = getVestedAmount(vestingId);
    uint256 remainingAmount = schedule.totalAmount - vestedAmount;

    uint256 vestedShare =
        vestedAmount * _depositedShares[vestingId] / schedule.totalAmount;
    uint256 sharesToRemove =
        _depositedShares[vestingId] - vestedShare;
    _totalTokenShares[schedule.token] -= sharesToRemove;

    _depositedShares[vestingId] = vestedShare;
    schedule.totalAmount = vestedAmount;
    schedule.cancelled = true;

    IERC20(schedule.token).safeTransfer(schedule.creator, remainingAmount);
}

결함은 혼합 계산 자체에 있습니다. 각 트랜치에는 고유한 토큰 수량, 베스팅 진행도, 그리고 그 트랜치가 추가될 당시 적용되던 풀 비율에 따라 발행된 지분 수량이 있습니다. 따라서 각 트랜치의 베스팅 완료 지분을 별도로 계산한 뒤 합산해야 합니다. 그러나 컨트랙트는 D 하나와 totalAmount 하나만 저장하기 때문에, 먼저 토큰 수량과 지분 기준값을 합산한 다음 결합된 포지션에 하나의 베스팅 비율을 적용합니다. 두 방식은 트랜치들의 토큰당 지분 비율이 같거나 베스팅 진행도가 같을 때에만 동등합니다. 토큰을 직접 전송하면 추가 예치 전에 풀 비율이 바뀌므로, 새 트랜치가 토큰당 받는 지분은 원래 트랜치보다 훨씬 적습니다. 이때 두 트랜치에 하나의 혼합 베스팅 비율을 적용하면, 두 트랜치의 베스팅 완료 지분 합계를 더 이상 나타내지 않는 V가 산출될 수 있습니다.

R는 해당 포지션의 누적 베스팅 완료 지분에서 이미 지급되었으므로, 일관된 일정이라면 R <= V을 충족해야 합니다. 이 부등식이 깨지는 것이 혼합 계산 오류의 직접적인 징후입니다. 코드는 포지션별 활성 지분 잔액을 별도로 관리하지 않고, 전체 _totalTokenShares에서 D - V을 직접 뺍니다. 다른 포지션들이 1000개의 지분에 기여하고, 이 포지션에는 D = 100와 R = 80이 있다고 가정해 보겠습니다. 취소 전 전체 공급량에는 1,000 + (D - R) = 1,020개의 지분이 포함됩니다. 혼합 계산 결과가 V = 60라면, cancel()는 D - V = 40을 빼서 전체 지분을 980개만 남깁니다. 이는 다른 포지션들이 기여한 1000개의 지분보다 20개 적습니다. 전체 공급량에 이 포지션의 활성 지분 20개만 있었다면 40개를 빼는 작업은 언더플로를 일으켜 되돌려졌을 것입니다. 초과 차감이 성공하는 이유는 공유 풀에 다른 포지션들이 기여한 지분이 있기 때문입니다. 이후 함수는 _depositedShares[vestingId] = 60을 저장하지만 schedule.released는 80으로 남아, 포지션 기록이 불가능한 상태인 D < R이 됩니다.

코드는 R <= V을 강제하지도 않고, sharesToRemove을 해당 포지션의 활성 지분 이내로 제한하지도 않습니다. 따라서 취소 과정에서 더 이상 전체 공급량에 포함되지 않는 과거 지분을 차감할 수 있습니다. 동시에 풀의 지분 비율을 통해 환불액을 환산하지 않고 totalAmount - vestedAmount을 토큰 단위로 직접 전송합니다. 취소 자체가 초과 지급을 일으키지는 않습니다. 취소는 전체 지분 공급량을 포지션들에 기록된 활성 지분 합계보다 낮춰, 이후 초과 상환이 가능한 조건을 만들 뿐입니다.

공격 분석

다음 분석은 Ethereum에서 발생한 대표 트랜잭션 0x352065...c23823을 사용합니다. 공격 컨트랙트는 이 트랜잭션에서 토큰별로 플래시 유동성을 사용해 토큰 5종에 동일한 핵심 절차를 실행했습니다.

  • 단계 1: 공격자는 즉시 베스팅이 완료되는 소규모 포지션 #5058 하나와, 즉시 베스팅이 완료되고 소프트 방식이며 추가 예치가 가능한 훨씬 큰 포지션 #5059 및 #5060를 생성했습니다. 소규모 포지션은 마지막 인출을 위해 유지하고, 대규모 포지션들은 전체 회계 처리를 왜곡하는 데 필요한 과거 지분 기록을 제공했습니다.

  • 단계 2: 공격자는 두 대규모 포지션에서 자산을 지급받았습니다. 초기 풀 비율이 거의 1:1일 때 각 지급으로 순예치액과 거의 같은 수량을 돌려받았습니다. 해당 지분은 _totalTokenShares에서 빠졌지만, 원래 값은 포지션 #5059과 #5060의 _depositedShares 기록에 남았습니다. 지급 사실은 각 일정의 별도 released 필드에만 기록되었습니다.

  • 단계 3: 공격자는 VestingManager로 토큰을 직접 전송했습니다. 이로써 지급된 지분은 복원하지 않은 채 토큰 잔액만 복원했으므로, 남아 있는 지분 한 개가 나타내는 토큰 수량이 급격히 늘었습니다.

  • 단계 4: 공격자는 이미 자산을 지급받은 대규모 포지션에 추가 예치했습니다. 추가 예치에는 새롭게 높아진 지분당 토큰 비율이 적용되었으므로, 상당한 수량의 토큰을 예치하고도 새로 발행된 지분은 소량에 그쳤습니다. 이제 이 포지션들에는 오래된 대규모 과거 기준값과 소규모 활성 지분 기여분이 결합되어 있었습니다.

  • 단계 5: 공격자는 추가 예치한 두 포지션을 취소했습니다. 각 취소에서는 예치 지분의 총합 기록에 하나의 혼합 베스팅 비율을 적용했습니다. 다시 계산된 vestedShare가 이미 지급된 지분보다 작아져, sharesToRemove이 해당 포지션의 활성 지분 기여분을 초과했습니다. 추가 예치 시 각 포지션이 추가한 활성 지분은 소량에 불과했지만, 전체 공급량은 포지션당 약 5.0e25개의 실체 없는 지분만큼 줄었습니다.

  • 단계 6: 분모가 실제보다 크게 낮아진 상태에서 공격자는 유지해 둔 소규모 포지션에서 자산을 지급받았습니다. 분석에 사용한 토큰 사례에서는 원시 단위로 약 5.73e22의 순예치액으로 약 1.07e26의 원시 단위를 인출했으며, 이는 예치액의 약 1,873배입니다. 공격자는 왜곡된 비율을 이용해 생성과 지급을 반복하고 플래시 유동성을 상환했습니다.

공식 사고 보고서는 확인된 자산 유출 트랜잭션 11건에서 동일한 패턴을 확인했습니다. 일부 변형은 하나의 트랜잭션 안에서 원자적으로 완료되었고, 다른 변형은 연결된 트랜잭션에서 마지막 지급을 실행했습니다. UNCX는 공격자의 원금, 일시적인 플래시 거래량, 반환된 자산을 제외한 실현 수익 또는 다른 경로로 이동한 수익을 기준으로 약 80.7609만 USD라는 수치를 계산했습니다 [1].

Phalcon Explorer 시작하기

트랜잭션을 자세히 살펴보고 현명하게 대응하세요

지금 무료로 사용해 보기

결론

근본 원인은 잘못된 총합 지분 회계 처리였습니다. 프로토콜은 서로 다른 토큰 대 지분 비율로 생성된 트랜치를 결합한 다음, 합산된 지분 기준값에 하나의 베스팅 비율을 적용했습니다. 이로 인해 누적 베스팅 완료 지분을 잘못 계산했고, 포지션이 여전히 기여하는 양보다 많은 지분을 제거할 수 있게 되었으며, 포지션별 기록과 전체 지분 공급량이 불일치했습니다. 공격자들은 실제보다 낮게 기록된 전체 공급량을 악용해, 다른 베스팅 포지션에 대한 지급을 충족하는 데 필요한 공유 잔액에서 토큰을 인출했습니다.

지분 비율이 달라지는 상황에서 후속 예치를 허용하는 회계 모델이라면, 트랜치들을 하나의 비율로 혼합하는 대신 각 트랜치의 토큰 수량, 지분 기준값, 베스팅 진행도를 보존해야 합니다. 또한 누적 지급량이 누적 베스팅 완료 지분을 초과하지 않도록 하고, 포지션이 자신의 활성 지분 기여분보다 많은 지분을 제거하지 못하도록 강제해야 합니다.

참고 자료

[1] UNCX Network Smart Token Vesting V2 사고 보고서

Web3를 위한 최고의 보안 감사 업체

출시 전 설계, 코드, 비즈니스 로직을 검증하세요

Sign up for the latest updates
안전한 보관을 넘어, 제1부: 안전한 결제 및 서명 워크플로 설계
Security Insights

안전한 보관을 넘어, 제1부: 안전한 결제 및 서명 워크플로 설계

공격자가 개인 키를 탈취하지 않고도 승인되지 않은 작업이 어떻게 유효한 서명을 얻을 수 있을까요? Beyond Secure Storage의 파트 1에서는 Bitget, Bybit, Binance 등의 사고를 통해 결제 요청, 트랜잭션 구성, 서명, 실행, 정산에 걸친 승인 절차의 실패를 살펴봅니다. 이어서 승인되지 않은 작업을 방지하고 손실을 제한하기 위한 설계 원칙을 도출하고, 검토, 감사, 침투 테스트, 훈련을 통해 이러한 보호 조치를 어떻게 검증할 수 있는지 설명합니다.

NEAR Intents 익스플로잇: 환불 처리 결함과 보안 교훈
Security Insights

NEAR Intents 익스플로잇: 환불 처리 결함과 보안 교훈

NEAR Intents 익스플로잇에서는 과도한 금액의 환불 이벤트를 이용해 예치 자산이 반환된 후에도 내부 잔액을 유지했습니다. 이 분석은 리시트 처리 흐름과 BNB Chain에서 발생한 총 3,865,021 USDT 규모의 출금 7건을 재구성하고, 이러한 자금 유출을 프로젝트 측이 보고한 전액 회수와 구분하며, NEAR 스마트 계약의 개발 및 감사에 필요한 보안 고려사항을 설명합니다.

약 557만 USD 손실: X100 Freezer·NEM 공격 | BlockSec 주간 보고서
Security Insights

약 557만 USD 손실: X100 Freezer·NEM 공격 | BlockSec 주간 보고서

이 주간 보고서는 2026년 10월 19일–2026년 10월 25일에 Ethereum과 BNB Chain에서 발생한 보안 사고 두 건을 다룹니다. 플래시론으로 자금을 조달해 X100 Freezer의 거버넌스를 장악한 사건과 BNB Chain의 NEM 토큰 유동성 풀에 영향을 미친 세금 정산 결함을 살펴봅니다. 보고서 범위에 포함된 추정 손실액은 X100의 대표 거래에서 발생한 약 475만 USD와 NEM 풀 사고에서 발생한 약 82.0만 USD를 합쳐 총 약 557만 USD입니다. 이 총액에는 별도로 보고된 후속 X100 자금 유출액은 포함되지 않습니다.

Best Security Auditor for Web3

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

BlockSec Audit