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
약 54만 2천 달러 손실: MakerDaoBot 공격 | BlockSec 주간 소식
Security Insights

약 54만 2천 달러 손실: MakerDaoBot 공격 | BlockSec 주간 소식

2026/10/05~10/11 이더리움 보안 사고 1건의 추정 손실은 약 54.2만 달러입니다. 공격 컨트랙트가 악성 어댑터를 접근 제어가 없는 MakerDaoBot 함수에 전달하자 봇이 내부 MakerDAO 잔액 권한을 부여했습니다. 어댑터는 기존 ETH-A 경매 4건을 정산하고 담보 200 ETH-A를 WETH로 전환했습니다. 공격 컨트랙트는 이를 언래핑해 공격자에게 200 ETH를 보냈습니다.

약 4억 1,800만 달러 손실: Bitget, NEAR Intents 해킹 사건 | BlockSec Weekly
Security Insights

약 4억 1,800만 달러 손실: Bitget, NEAR Intents 해킹 사건 | BlockSec Weekly

이번 격주 보안 리포트는 2026년 9월 21일부터 10월 4일까지 발생한 8건의 사고를 다루며, 이더리움, BNB 체인, 솔라나, 비트코인, 트론, XRP 레저, 지캐시, 아발란체, 니어 등 네트워크에서 약 4억 1,840만 달러의 손실이 발생했습니다. Bitget의 3억 8,750만 달러 규모 제3자 보안 제품 취약점과 NEAR Intents의 387만 달러 환불 검증 결함 및 상태 롤백 누락 문제를 상세히 분석합니다.

Bitget의 3억 8,750만 달러 오프체인 침해 사고: 키와 컨트랙트를 넘어서
Security Insights

Bitget의 3억 8,750만 달러 오프체인 침해 사고: 키와 컨트랙트를 넘어서

2026.9.24, 공격자가 제3자 보안 제품 취약점을 악용해 내부 자격증명을 탈취하고 출금 명령을 위조했습니다. 이더리움·EVM·XRP Ledger·Zcash·TRON상 Bitget 지갑에서 약 3.875억 달러가 인출됐으나 개인키·콜드월렛은 무사했습니다. 이 글은 사건 경위·자금 흐름과 자산 전환·복구 대응을 분석하고, 기관용 심층 방어 체계를 제안하며 승인된 블록체인 침투 테스트의 검증 역할을 설명합니다.

Best Security Auditor for Web3

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

BlockSec Audit