2025년 9월 2일, Bunni V2 프로토콜이 정교한 익스플로잇 공격을 받았습니다 [1]. 공격자는 유동성 회계 메커니즘의 치명적인 취약점을 악용하여 두 개의 유동성 풀에서 약 840만 달러를 탈취했습니다: 이더리움의 USDC/USDT 풀 [2]과 Unichain의 weETH/ETH 풀 [3]이 그 대상이었습니다.
근본 원인은 유동성 제거 시 프로토콜이 유휴 풀 잔액을 업데이트하는 과정에서 발생한 반올림 오류였습니다. 이 오류로 인해 컨트랙트 내 총 유동성이 크게 과소평가되었고, 이론적 유동성과 실제 유동성 사이에 악용 가능한 불일치가 발생했습니다. 공격자는 이 불일치를 이용해 정밀한 샌드위치 공격을 실행하여 이익을 취했습니다.
이 사건은 Bunni 프로토콜에 심각한 재정적 손실을 직접적으로 초래했으며, 프로토콜은 이후 2025년 10월 23일 파산을 선언했습니다 [4].
배경
Bunni V2는 Uniswap V4를 기반으로 구축된 자동화 시장 조성자(AMM) 프로토콜입니다. 훅(hook) 메커니즘을 통해 핵심 로직을 구현하며, 유동성 공급자(LP)에게 향상된 자본 효율성을 제공하기 위해 Uniswap V3의 집중 유동성 알고리즘을 개선한 혁신을 도입했습니다 [5].
구체적으로, 이 프로토콜은 주로 재담보(Rehypothecation) 기능과 리밸런싱(Rebalancing) 메커니즘을 통해 LP 수익을 향상시킵니다. 전자는 기본 유동성을 확보하는 동시에 추가적인 외부 수익을 창출하기 위해 유동성을 외부 수익 창출 프로토콜에 할당합니다. 후자는 수수료 수입을 높이기 위해 자본의 능동적 활용도를 높이는 방식으로 가격 범위에 걸친 유동성 분배를 지속적으로 최적화합니다. 이 두 메커니즘은 집중 유동성 모델이라는 기초 위에 세워진 프로토콜의 핵심 혁신을 구성합니다.
재담보(Rehypothecation)
유동성 공급자의 수익을 향상시키기 위해 Bunni V2는 재담보 전략을 채택합니다. 이 전략은 자금을 서로 다른 포지션에 할당합니다:
- rawBalance: 토큰에 대한 풀 준비금의 일부는 Uniswap V4의
contract PoolManager내에 직접 저장됩니다. 이는 스왑을 원활히 처리하기 위한 즉시 사용 가능한 유동성으로 기능합니다. - reserves: 나머지는 지정된 ERC4626 볼트에 예치됩니다. 이를 통해 사용자는 해당 자산에서 추가적인 외부 수익을 얻을 수 있습니다.
따라서 풀의 총 자산은 다음과 같이 정의됩니다: 풀 자산 = rawBalance + reserves의 기초 자산 금액.
리밸런싱(Rebalancing)
수수료 수익을 높이기 위해 Bunni V2는 리밸런싱 메커니즘을 구현합니다. 이 메커니즘은 시간 가중 평균 가격을 모니터링합니다. 가격 변화가 임계값을 초과하면, 유동성 분배 함수(LDF)에 따라 유동성이 서로 다른 가격 범위에 재분배됩니다.
이 재분배는 LDF가 요구하는 토큰 비율을 변경할 수 있으며, 한 토큰에서 잉여분이 발생할 수 있습니다. 이 잉여분을 유휴 잔액(idle balance)이라 정의합니다.
따라서 유동성은 두 부분으로 나뉩니다:
- 능동 잔액(Active Balance): 유동성 계산에 참여하는 LDF에 의해 할당된 부분.
- 유휴 잔액(Idle Balance): 능동 유동성에 사용되지 않는 잉여분.
따라서 풀 자산 = 능동 잔액 + 유휴 잔액입니다.
핵심 함수: 유동성 계산 및 제거
이 공격은 두 가지 핵심 함수인 queryLDF()와 withdraw()를 악용합니다. queryLDF() 함수는 스왑을 위한 풀의 유동성을 계산하며, withdraw() 함수는 사용자가 비례적으로 유동성을 제거할 수 있도록 합니다.
함수 queryLDF()
재담보 전략으로 인해 기초 자산의 수량은 동적이며, Bunni V2는 고정된 "총 유동성" 값을 저장하지 않습니다. 대신, 프로토콜은 스왑 발생 시 실시간 유동성을 조회하기 위해 queryLDF() 함수를 제공합니다 [6]. 이 함수의 실행 과정은 다음 네 단계로 구성됩니다:
-
유동성 밀도 조회:
-
유동성 밀도 함수
ldf.query()를 호출하여 현재 가격 틱 범위 외부의 유동성 밀도를 얻습니다. -
LiquidityAmounts.getAmountsForLiquidity()를 호출하여 현재 틱 범위 내의 밀도를 얻습니다. -
양방향에서 token0과 token1의 총 유동성 밀도를 계산하며, 이를
totalDensity0및totalDensity1로 표기합니다.
특히,
LiquidityAmounts.getAmountsForLiquidity()함수는 올림(round up)을 사용하여 계산된 토큰 수량이 보수적으로 이론값보다 낮아지지 않도록 보장합니다.
-
-
가용 잔액 계산
유동성 계산에 사용되는 가용 잔액은
balance0및balance1로 표기됩니다. 유동성 계산에 참여하지 않는 자금을 제외하기 위해 유휴 잔액이 해당 토큰의 총 잔액에서 차감됩니다.이 공격에서 풀의 유휴 자금이
token0으로 구성된 경우, 계산 공식은 다음과 같습니다: -
-
유효 유동성 추정
-
실제 가용 잔액(
balance0또는balance1)과 계산된 총 밀도(totalDensity0또는totalDensity1)를 기반으로 각 토큰이 지원할 수 있는 유동성을 추정합니다. -
두 추정값 중 더 작은 값을 최종 유효 총 유동성으로 선택합니다.
공식은 다음과 같습니다:
-
-
능동 잔액 계산
결정된 총 유동성을 기반으로, 프로토콜은 거래에 사용 가능한 실제 토큰 수량을 계산합니다. 이를 능동 잔액(Active Balance)이라 정의합니다.
함수 withdraw()
Bunni V2는 유동성 제거를 위해 withdraw() 함수를 제공합니다. 사용자는 풀의 총 자금에서 자신의 지분 비율에 따라 유동성을 제거합니다. 프로토콜은 동일한 비율로 rawBalance, reserves, idleBalance를 업데이트합니다. 조정 공식은 다음과 같습니다:
여기서:
shares는 사용자가 제거하는 유동성 지분의 수량입니다;totalSupply는 해당 풀의 유동성 토큰 총 공급량입니다.
취약점 분석
취약점은 withdraw() 함수가 유휴 잔액의 조정 금액을 계산할 때 내림(floor rounding, 즉 버림) 방식을 사용하는 데서 비롯됩니다. 이로 인해 유휴 잔액이 과대평가됩니다.
가용 잔액 공식인 를 상기하면, 과대평가된 유휴 잔액은 유동성 계산에 사용되는 가용 잔액(balance0)을 직접적으로 과소평가하게 만듭니다. 결과적으로 추정된 유효 총 유동성도 과소평가됩니다. Bunni 익스플로잇 사후 분석 보고서 [7]에 따르면, 유동성 계산에서 이 반올림 방향은 의도적으로 채택된 것이었습니다. 계산된 유동성 값이 낮을수록 스왑 시 가격 영향이 더 크게 나타납니다.
이 설계는 두 토큰 간의 잔액 비율이 상대적으로 균형을 유지한다는 중요한 가정에 의존합니다. 충분한 유동성이 있는 일반적인 조건에서는 각 토큰에 대해 개별적으로 추정된 총 유동성 값이 일반적으로 근접합니다. 따라서 반올림 오류의 영향은 제한적입니다. 그러나 유휴 잔액을 보유한 토큰의 가용 잔액이 극도로 낮아지면 결함이 드러납니다. 이 시나리오에서는 내림 반올림 오류가 크게 증폭됩니다.
공격자는 이 취약점을 이용해 일련의 소규모 인출을 수행하여, 내림 방식으로 token0의 가용 잔액을 28 wei에서 4 wei로 낮췄습니다. 이 감소는 실제로 소각된 유동성 지분의 비율(즉, 8.998105442969973e-07%)을 훨씬 초과했습니다. 한편 token1의 가용 잔액은 상대적으로 정상적인 수준을 유지했습니다. 이 불균형이 상당한 차익거래 기회를 만들어냈습니다. 다음 장에서 자세한 수치 분석을 제공합니다.
공격 분석
이더리움 트랜잭션 [2]을 예로 들면, 공격자는 세 단계의 공격을 실행했습니다:
- 첫 번째 단계에서 공격자는 가격 조작을 수행하여 USDC의 가용 잔액(token0)을 크게 고갈시켰습니다. 이는 이후 반올림 오류를 증폭시키는 데 필요한 초기 조건을 만들었습니다.
- 두 번째 단계에서는 일련의 소규모 인출을 통해 핵심 익스플로잇이 수행되어, 프로토콜이 풀의 실제 유동성을 과소평가하도록 유도했습니다.
- 세 번째 단계에서 공격자는 두 방향의 스왑을 실행하여 프로토콜의 과소평가된 유동성과 풀의 실제 유동성 사이의 불일치를 차익거래하고, 최종적으로 이익을 추출했습니다.
1단계: 가격 조작 및 목표 토큰 잔액 감소
공격자는 세 번의 스왑 트랜잭션을 실행하여 USDT(token1) 대비 USDC(token0)의 가격을 조작하고, 초기 틱 = -1에서 틱 = 5000으로 몰아붙였습니다. 주요 목적은 풀의 USDC 능동 잔액을 고갈시켜 28 wei라는 극도로 낮은 수준으로 줄이는 것이었습니다. 이는 다음 단계에서 반올림 오류를 증폭시키는 데 필요한 초기 조건을 만들었습니다.
2단계: 인출을 악용한 유동성 불일치 증폭
공격자는 withdraw() 함수를 통해 44번의 소규모 인출을 시작했습니다. 이 함수가 idleBalance를 업데이트할 때 내림(floor rounding)을 사용하기 때문에, 프로토콜의 유휴 잔액이 과대평가되었습니다. 이로 인해 queryLDF() 함수에서 USDC 가용 잔액이 더욱 과소평가되었습니다. 이러한 반복 작업 후, USDC 가용 잔액은 28 wei에서 4 wei로 비정상적으로 억제되었습니다. 이는 실제 85.7%의 감소를 나타내며, 제거된 유동성 지분에 해당하는 이론적 비율(즉, 8.998105442969973e-07%)을 훨씬 초과했습니다. 이 시점에서 풀의 USDC로부터 추정된 유동성은 심각하게 과소평가되었습니다.
3단계: 차익거래 실행 및 수익 실현
공격자는 이어서 두 방향의 스왑을 실행하여 샌드위치 공격과 유사한 작업을 구성했습니다.
Step1: 공격자는 대량의 USDT를 사용하여 USDC로 스왑했습니다. 이 시점에서 내부 유동성 계산은 과소평가된 USDC 잔액을 기반으로 심각하게 저평가된 상태였습니다. 이 대규모 스왑은 가격을 극단으로 몰아붙여 틱을 5,000에서 839,189로 이동시켰습니다.
Step2: 극단적인 가격이 형성된 후, 공격자는 즉시 작업을 역전시켜 USDC의 일부를 USDT로 다시 스왑했습니다. 풀의 가격이 이제 심각하게 잘못 정렬되었으므로, queryLDF() 함수의 USDC 유동성 밀도 반환값이 1로 떨어졌습니다. 이로 인해 USDC를 기반으로 추정된 유동성 값이 USDT를 기반으로 추정된 값보다 커졌습니다.
더 작은 값을 선택하는 프로토콜의 로직에 따라, 총 유동성은 USDT 잔액에 의해 결정됩니다. 이로 인해 계산된 유동성이 과소평가 상태에서 즉시 정상 수준으로 되돌아가 갑작스러운 증가가 발생했습니다. 공격자는 이 변화를 이용하여 최소한의 USDC를 교환해 대량의 USDT를 얻었으며, 이로써 차익거래를 완료하고 수익을 실현했습니다.
요약
이 사건은 궁극적으로 유동성 제거 시 유휴 잔액 조정에서 발생한 반올림 오류로 인해 발생했습니다. 이 내림 함수 설계는 유동성 계산에서 보안 전략으로 의도된 것이었지만, 중요한 경계 조건을 충분히 고려하지 못했습니다. 구체적으로, 토큰 잔액이 심각하게 불균형할 때 반올림 오류가 비선형적으로 증폭됩니다.
이 사건은 복잡한 DeFi 프로토콜에서 여러 모듈 간의 결합 위험을 드러냅니다. 개별 구성 요소의 반올림 규칙이 보수적으로 설계되었더라도, 전체 시스템에 걸쳐 일관된 보안 검증이 이루어지지 않으면 특정 상황에서 악용될 수 있는 치명적인 취약점이 발생할 수 있습니다.
참고 자료
-
https://etherscan.io/tx/0x1c27c4d625429acfc0f97e466eda725fd09ebdc77550e529ba4cbdbc33beb97b
-
https://uniscan.xyz/tx/0x4776f31156501dd456664cd3c91662ac8acc78358b9d4fd79337211eb6a1d451
BlockSec 소개
BlockSec은 풀스택 블록체인 보안 및 암호화폐 컴플라이언스 전문 업체입니다. 저희는 고객이 프로토콜 및 플랫폼의 전체 라이프사이클에 걸쳐 코드 감사(스마트 컨트랙트, 블록체인 및 지갑 포함), 실시간 공격 차단, 사고 분석, 불법 자금 추적, AML/CFT 의무 준수를 수행할 수 있도록 지원하는 제품과 서비스를 구축합니다.
BlockSec은 권위 있는 학술 컨퍼런스에서 다수의 블록체인 보안 논문을 발표하고, DeFi 애플리케이션의 여러 제로데이 공격을 보고했으며, 2천만 달러 이상을 구조하기 위해 다수의 해킹을 차단하고, 수십억 달러 상당의 암호화폐를 안전하게 보호했습니다.
-
공식 웹사이트: https://blocksec.com/
-
공식 트위터 계정: https://twitter.com/BlockSecTeam
-
🔗 BlockSec 감사 서비스 : 요청 제출



