Back to Blog

작은 반올림 오류, 큰 펀드 손실: 최근 Balancer 사건에 대한 심층 분석

Code Auditing
September 14, 2023
11 min read

2023년 9월 15일 업데이트: Balancer가 공식 사후 분석 보고서를 공개했습니다. 이 보고서는 이번 사건의 전체 경위와 함께 경험 및 교훈을 상세히 설명합니다. 복잡하면서도 탁월한 서술 방식을 갖춘 이 사후 분석 보고서는 매우 인상적이며, 읽어볼 충분한 가치가 있습니다.

보안 관점에서, 이 사후 분석 보고서는 두 가지 버그가 존재함을 밝히고 있습니다. 첫 번째는 본 보고서에서 논의한 내림(rounding down) 오류이며, 두 번째는 본 보고서의 공격 단계 3.6 및 3.7에서 발생한 "0 공급량에서 비율 초기화(resets rate on 0 supply)"입니다. Balancer의 보고서는 두 번째 문제를 가장 핵심적인 이슈로 보고, 첫 번째는 기여 요인으로 간주합니다. 그러나 우리는 수익성 있는 익스플로잇을 위해 두 버그 모두 동등하게 중요하다고 생각합니다:

  1. 첫 번째 버그는 토큰 비율을 부풀리는 데 사용되며, 수익의 근본 원인으로 작용합니다. 이 버그 없이는 수익을 창출하는 것이 불가능합니다.

  2. 두 번째 버그는 bb-a-토큰의 부채를 균형 있게 조정함으로써 익스플로잇을 가능하게 합니다. 이 버그 없이는, bb-a-토큰의 유동성 부족으로 인해 공격이 실패할 것입니다. 이는 공격자가 다른 방법으로 해당 토큰을 확보하지 않는 한, 이 토큰을 얻을 수 있는 다른 출처가 없기 때문입니다.

2023년 8월 22일, Balancer는 여러 부스티드 풀(boosted pool)에 영향을 미치는 심각한 취약점의 존재를 공개적으로 발표하고, 영향받은 풀에서 즉시 LP를 출금할 것을 사용자들에게 촉구했습니다. Balancer는 대부분의 TVL을 보호하기 위한 긴급 완화 절차를 시작했지만, 일부 자금은 여전히 위험에 노출되어 있었습니다. 안타깝게도, 5일 후인 8월 27일, 실제 환경에서 여러 건의 공격이 발생한 것을 확인했습니다. 이후 $2.12M을 초과하는 자산이 탈취되었습니다.

본 보고서 작성 시점(발표 이후 3주 이상이 지난, 공개가 안전하다고 판단되는 시점)에서 Balancer는 이 취약점에 대한 심층 분석을 공개하지 않은 상태입니다. 본 보고서에서는 주로 공격 트랜잭션 중 하나를 기반으로 포괄적인 분석을 제공하고자 합니다.

핵심 요약 (TL;DR)

  • 조사 결과, 근본 원인은 linear 풀의 내림 로직(rounding down logic)으로 인한 가격 조작에 있으며, 이로 인해 해당 boosted 풀에서 사용하는 캐시된 토큰 비율이 부적절하게 영향을 받는 것으로 나타났습니다.
  • 이번 사건은 취약한 소스에서 포크된 프로젝트에 대한 신속한 알림의 중요성을 강조하며, 이는 전체 커뮤니티에 상당한 도전 과제임이 분명합니다.
  • 지속적으로 발생하는 공격들은 사전 위협 예방의 필요성을 강조하며, 이는 잠재적 손실을 완화하는 데 필수적으로 기여할 수 있습니다.

다음 섹션에서는 먼저 Balancer에 대한 필수 배경 정보를 제공합니다. 그 후, 취약점 및 관련 공격에 대한 포괄적인 분석을 수행할 것입니다. 마지막으로, 현재까지 관찰된 공격과 그에 따른 수익에 대한 간략한 요약을 제공합니다.

0x1 Balancer 배경

Balancer V2 [1]는 프로그래밍 가능한 유동성을 위한 유연한 빌딩 블록을 나타내는 탈중앙화 자동화 시장 조성자(AMM) 프로토콜입니다. 토큰 회계가 풀 로직과 결합되어 있는 다른 AMM과 달리, Balancer는 토큰 회계 및 관리를 풀 로직과 분리하여 많은 토큰 전송을 줄임으로써 스왑 효율성을 향상시킬 수 있습니다.

Balancer는 다양한 유형의 풀을 지원합니다. 각 풀은 BPT(즉, Balancer Pool Token)라는 LP 토큰과 연관되어 있습니다. 기본적으로 BPT 가치는 모든 기초 토큰의 총 가치를 기반으로 계산됩니다.

Balancer는 Vault에 등록된 모든 풀에서 최적 가격을 활용하는 batch swaps라고도 알려진 멀티 홉 스왑을 지원합니다. 특히, Vault는 멀티 홉 스왑을 용이하게 하기 위해 batchSwap 함수를 제공합니다.

Balancer 풀의 flash swap은 스왑을 실행하기 위해 전통적으로 필요한 입력 토큰을 보유할 필요성을 없애줍니다. 대신, 불균형을 식별하면 Vault에 스왑을 실행하도록 지시하고 이후 보상을 받을 수 있습니다.

0x1.1 Balancer의 다양한 풀

다음에서는 이 취약점과 관련된 풀의 몇 가지 개념을 간략하게 소개합니다.

  • Linear 풀: Linear 풀 [2]은 알려진 환율로 자산과 그 래핑된 수익 창출 대응물 간의 교환을 용이하게 하는 Balancer 풀입니다. 이름에서 알 수 있듯이, Linear 풀은 선형 수학(Linear Math)을 사용합니다. linear 풀은 다음을 포함한 세 가지 토큰을 보유합니다:

    • 두 가지 자산, 즉 동일한 가치의 기초 토큰을 가진 mainwrapped 토큰;
    • 해당 BPT(Balancer Pool Token). 참고로 BPT는 ERC-20 토큰입니다.
  • Linear 풀 중첩(Nesting): Linear 풀의 BPT는 다른 풀 내에 중첩될 수 있습니다. 스와퍼가 BPT에서 Linear 풀의 기초 토큰 중 하나로 스왑할 수 있기 때문에, 이는 기초 자산과 외부 풀의 토큰 사이에 간단한 batchSwap 경로를 생성합니다.

  • Composable Stable 풀: Composable Stable 풀 [3]은 근사 등가 또는 알려진 환율로 일관되게 스왑될 것으로 예상되는 자산을 위해 설계되었습니다. Composable Stable 풀은 안정적인 수학(Stable Math)을 사용하여 상당한 가격 영향 없이 상당한 규모의 스왑을 허용하며, 동종 및 상관 관계가 있는 스왑에 대한 자본 효율성을 크게 향상시킵니다.

    풀은 자체 LP 토큰과의 스왑을 허용할 때 Composable 풀이 됩니다. LP 토큰을 다른 풀에 넣거나("중첩") 하면 중첩된 풀 토큰에서 외부 풀의 토큰으로 쉽게 batchSwap할 수 있습니다.

  • Boosted 풀: Boosted 풀 [4]은 대규모 풀의 유휴 유동성에 대한 자본 효율성을 향상시키기 위해 설계되었습니다. Boosted 풀은 실제로 다른 풀의 하위 클래스입니다. 예를 들어, boosted 풀은 linear 풀 위에 구축될 수 있습니다.

    Boosted 풀은 사용자가 일반 토큰에 대한 스왑 유동성을 제공하면서 유휴 토큰을 외부 프로토콜로 전달할 수 있도록 함으로써 높은 자본 효율성을 제공하도록 설계되었습니다. 이를 통해 유동성 공급자는 스왑에서 수집하는 스왑 수수료 외에도 Aave와 같은 프로토콜의 혜택을 누릴 수 있습니다.

0x1.2 취약한 Boosted 풀의 구체적인 예시: Balancer Boosted Aave USD

Balancer Boosted Aave USD (심볼: bb-a-USD)는 유휴 유동성을 Aave로 전송하면서 세 가지 스테이블코인(즉, USDC, USDT, DAI) 간의 스왑을 용이하게 하는 Composable Stable 풀입니다. 기초 linear 풀은 다음과 같습니다:

  • bb-a-USDC (USDC와 래핑된 aUSDC로 구성)
  • bb-a-USDT (USDT와 래핑된 aUSDT로 구성)
  • bb-a-DAI (DAI와 래핑된 aDAI로 구성)

구체적으로, bb-a-USD는 세 개의 서로 다른 linear 풀의 풀 토큰을 포함하는 하나의 Composable Stable 풀의 집합이며, 각 linear 풀에는 DAI, USDC, USDT라는 연관된 스테이블 토큰이 있습니다. 공식 문서 [5]에서 제공하는 아래 그림은 bb-a-USD의 구조를 보여줍니다:

0x1.3 BPT 가격 계산 방법

자연스럽게 제기되는 중요한 질문은 특정 금액(즉, amountIn)의 BPT를 특정 금액(즉, amountOut)의 다른 토큰으로 스왑할 때 BPT의 가격을 어떻게 결정하는가입니다.

Balancer는 서로 다른 풀에서 채택한 수학적 공식에 대한 자세한 설명을 제공합니다 [6, 7]. 간단하게 설명하기 위해, 여기서는 가장 관련성 높은 개념을 요약합니다.

linear 풀을 예로 들면, BPT의 가격은 LinearPool 컨트랙트의 onSwap 함수에서 계산됩니다.

계산은 다음과 같이 요약할 수 있습니다:

amountOut=amountIntokenRateamountOut=amountIn*tokenRate

여기서 tokenRate는 다음 공식으로 계산됩니다:

_INITIAL_BPT_SUPPLY\_INITIAL\_BPT\_SUPPLY는 상수 값입니다: 211212^{112} - 1.

위 공식에서 분자는 main 토큰의 잔액과 wrapped 토큰의 잔액의 합으로 단순화할 수 있으며, 분모는 미리 정의된 값(즉, _INITIAL_BPT_SUPPLY)과 BPT 잔액의 차이입니다.

관련된 모든 토큰의 잔액은 계산을 수행하기 전에 정규화(nominalized) 되어야 한다는 점에 주목할 필요가 있습니다. 이는 서로 다른 토큰이 서로 다른 소수점 자리수를 가질 수 있기 때문입니다. 구체적으로, 주어진 토큰의 원시 잔액은 _scalingFactors 함수에 의해 결정되는 해당 **업스케일 팩터(upscale factor)**와 곱해집니다.

(1) Linear 풀의 스케일링 팩터

BPTmain 토큰 모두 일반적이고 일정한 스케일링 팩터를 가집니다.

(2) bb-a-USD와 같은 Boosted 풀의 스케일링 팩터

boosted 풀의 계산은 조금 더 복잡합니다. 구체적으로, 반환되는 스케일링 팩터는 원시 스케일링 팩터(예: 1e18)와 토큰 비율의 곱이며, 이는 캐시된 토큰 비율이 있는 경우 그것으로부터 얻어집니다.

캐시된 토큰 비율은 어디서 오는 것일까요? _updateTokenRateCache라는 프라이빗 함수가 존재합니다. 분명히, 이 함수는 먼저 해당 토큰의 getRate 함수를 호출하여 비율을 검색한 다음 캐시합니다.

다시 bb-a-USDC를 예로 들면, 해당 getRate 함수의 핵심 로직은 앞서 논의한 공식을 따릅니다.

_updateTokenRateCache 함수를 트리거할 수 있는 세 가지 가능한 경로가 있다는 점에 주목하십시오:

또한, onSwap 함수를 통한 경로에서 업데이트를 수행할 때 만료 확인이 이루어집니다:

0x2 취약점 분석

근본 원인은 linear 풀의 onSwap 함수 내의 내림 로직(rounding down logic)으로 인한 가격 조작에 있습니다. 이는 결국 boosted 풀에서 사용하는 캐시된 토큰 비율에 부적절한 영향을 미칩니다.

구체적으로, _downscaleDown 함수가 호출될 때 amountOut이 내림됩니다. 따라서 amountOutscalingFactors[indexOut] 사이에 상당한 크기 차이가 있는 경우, _downscaleDown 함수의 반환 값이 0이 될 수 있습니다.

예를 들어, bb-a-USDC 풀에서 bb-a-USDC(as BPT)를 사용하여 USDC(as main token)로 스왑하는 경우, amountOut이 1,000,000,000,000보다 작으면 반환 값은 항상 0으로 내림됩니다. 이는 bb-a-USDC를 단방향으로 유동성을 추가하는 것으로 볼 수 있어 bb-a-USDC의 잔액이 증가하게 됩니다.

그 결과, BPT가 스왑에 사용되는 토큰인 경우, 비율을 계산하는 공식에 따라 그 비율은 상승할 것입니다. 분자가 동일하게 유지되는 동안 분모가 감소하기 때문입니다. 이 버그는 (큰) 가격 차이를 발생시키기 위해 악용될 수 있습니다.

0x3 공격 분석

공격 트랜잭션은 다음과 같은 공격 단계로 구성됩니다:

  1. Aave에서 플래시론을 통해 300,000 USDC 차입.
  2. bb-a-USDC 풀에서 1.067753 USDC를 0.970495 aUSDC로 스왑.
  3. bb-a-USDCbb-a-USD 풀에서 batchSwap 수행, 즉 42,203 USDC로 15,628 bb-a-USDC, 139,431 bb-a-DAI, 248,868 bb-a-USDT 획득. 세부 단계는 다음 표에 요약되어 있습니다(소수점 포함):
  1. LP 토큰을 해당 기초 스테이블 토큰으로 스왑:
  • bb-a-DAI 풀에서 139,431 bb-a-DAI -> 141,127 DAI
  • bb-a-USDC 풀에서 15,628 bb-a-USDC -> 15,685 USDC
  • bb-a-USDT 풀에서 248,868 bb-a-USDT -> 253,461 USDT
  1. 플래시론 상환 후 최종 수익:
  • 114,324 DAI
  • 253,461 USDT
  • 0.970495 aUSDC

공격자가 단계 2에서 bb-a-USDC 풀에서 USDCaUSDC를 소진시킨 것은 주목할 만한데, 이는 단계 3의 가격 조작을 훨씬 쉽게 만듭니다. 즉, 공격자는 USDCbb-a-USDC에만 집중하면 됩니다.

단계 3이 핵심 역할을 합니다. 이제 공격자가 어떻게 수익을 얻을 수 있었는지 파악하기 위해 이 단계의 세부 사항을 살펴보겠습니다. 구체적으로,

  • 단계 3.1은 bb-a-USDC 풀에서 bb-a-USDCUSDC를 소진시키는 데 사용됩니다;
  • 단계 3.3과 3.4는 bb-a-USDCbb-a-DAI로 스왑하는 데 사용되며, 단계 3.5는 bb-a-USDCbb-a-USDT로 스왑하는 데 사용됩니다.
  • 단계 3.7은 bb-a-USDC 풀에서 USDCbb-a-USDC로 스왑하는 데 사용됩니다.

여기서 단계 3.2와 3.6은 앞서 논의한 내림(rounding down)으로 인해 대상 토큰(즉, USDC)을 다시 스왑하지 않으므로, 스왑 후에도 대상 토큰의 잔액은 변하지 않습니다. 이는 bb-a-USDC 풀에 bb-a-USDC의 추가 유동성을 공급하는 것으로 볼 수 있습니다.

분명히, 비정상적인 스왑은 주로 단계 3.4, 3.5, 3.7에서 발생합니다. 다음에서는 이 각 단계의 세부 사항을 차례로 살펴보겠습니다.

(1) bb-a-USDC -> bb-a-DAI

단계 3.3에서 bb-a-USDCbb-a-DAI 사이의 환율은 거의 1이지만, 단계 3.4에서는 환율이 19가 됩니다:

  • 단계 3.3: 1,000,339,378,515,783,699 / 1,000,000,000,000,000,000 = 1.00
  • 단계 3.4: 139,430,482,942,020,211,267,110 / 7,300,000,000,000,000,000,000 = 19.10

앞서 논의한 코드 로직을 상기하면, 단계 3.3에서 이전에 캐시된 토큰 비율을 반환하여 스케일링 팩터(1,012,181,365,780,643,700)를 계산한 후, 새 값(40,240,000,000,000,000,000)을 계산하기 위해 비율을 업데이트합니다. 이 업데이트된 값은 단계 3.4에서 새 스케일링 팩터로 사용됩니다. 원시 스케일링 팩터가 변경되지 않으므로(즉, 1e18), 이는 새 비율이 이전 비율보다 약 40배 크다는 것을 의미합니다.

그러나 이 상당한 증가는 어디서 비롯된 것일까요? tokenRate를 계산하는 공식을 다시 살펴보겠습니다. 단계 2에서 aUSDC 잔액이 소진되었으므로, tokenRate 계산은 다음과 같이 단순화할 수 있습니다:

여기서 nominalMainBalance의 실제 값은 단계 3.2에서 발생한 내림(rounding down)으로 인한 것입니다.

(2) bb-a-USDC -> bb-a-USDT

단계 3.5는 동일한 트릭을 사용하여 더 많은 bb-a-USDT를 얻으며, bb-a-USDCbb-a-USDT 사이의 환율은 12 이상입니다:

  • 248,868,905,733,352,246,491,156 / 20,000,000,000,000,000,000,000 = 12.44

(3) USDC -> bb-a-USDC

또한, 단계 3.6에서 bptBalance가 증가하고, 단계 3.7에서 bptSupply가 0이 됩니다. 이렇게 함으로써 USDC를 거의 1:1 환율로 bb-a-USDC로 스왑하는 것이 가능해집니다.

0x4 공격 및 수익 요약

이 보고서를 작성하는 시점까지, 실제 환경에서 수십 건의 공격을 관찰했으며, $2.12M을 초과하는 손실을 초래했습니다. 요약하면, 이러한 공격은 세 개의 서로 다른 계정에 의해 실행되었으며, 다음과 같습니다:

Balancer는 이 취약점으로 인해 총 ~$1M의 손실을 입었습니다. Balancer에 대한 첫 번째 공격 후 12시간도 채 안 되어, 포크된 프로토콜인 Beethoven X도 유사한 공격에 굴복하여 약 ~$1.1M의 손실을 입은 것으로 추정됩니다. Beethoven X는 Balancer보다 더 큰 손실을 입었습니다! 이 보안 사고로 인한 누적 손실은 ~$2.12M에 달했습니다.

이 공격 트랜잭션의 전체 목록은 우리가 준비한 문서에 수집되어 있습니다. 더 자세한 정보는 해당 문서를 참조하십시오.

공격자에 대한 몇 가지 관찰

각 네트워크에서 시작된 트랜잭션을 분석한 결과, Fantom의 공격 트랜잭션 추적이 EthereumOptimism의 것과 상당히 다르다는 것을 발견했습니다.

구체적으로, 핵심 함수의 주목할 만한 차이점 외에도, Fantom의 공격자는 MEV 봇에 의해 선취(front-run)되는 것을 방지하기 위해 두 가지 고유한 트릭을 활용했습니다. 또한, Fantom 공격에 사용된 자금은 공격 163일 전에 미리 준비되어 있었습니다.

위에서 상세히 기술된 관찰로부터 다음을 추론할 수 있습니다:

  • 최소 두 명의 서로 다른 공격자가 관여했습니다.

  • Fantom의 공격자는 경험 많은 연쇄 범죄자입니다.

0x5 결론

요약하면, 이것은 내림 로직(rounding down logic)에 뿌리를 둔 미묘한 취약점입니다. 그러나 이 취약점을 악용하는 것은 간단하지 않습니다. 구체적으로, 공격자는 linear 풀의 내림 문제를 악용하여 캐시된 토큰 비율을 부풀려서 해당 boosted 풀의 토큰 가격을 조작할 수 있었습니다.

이 사건은 또한 취약한 소스에서 포크된 프로젝트에 대한 적시 통보의 중요성을 강조합니다. Balancer의 경고에도 불구하고, 포크된 프로토콜을 겨냥한 공격이 계속되고 있으며, 이는 이러한 포크된 프로젝트들이 소스 프로젝트의 보안 업데이트에 대해 정보를 계속 유지해야 할 필요성을 강조합니다. 그러나 이러한 포크된 프로젝트들이 신속한 알림을 받도록 보장하는 것은 커뮤니티에 지속적인 도전 과제입니다.

또한, 지속적인 일련의 공격들은 잠재적인 손실을 효과적으로 완화하는 데 도움이 될 수 있는 사전 위협 예방의 중요성을 강조합니다.

참고문헌

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