Back to Blog

또 다른 정밀도 손실의 비극: KyberSwap 사건에 대한 심층 분석

Code Auditing
December 5, 2023
10 min read

2023년 11월 23일, 우리는 KyberSwap을 대상으로 한 일련의 공격을 관찰했습니다. 이 공격들로 인해 총 $48M 이상의 손실이 발생했습니다. 우리의 초기 분석에서는 해당 익스플로잇이 틱 조작 및 이중 유동성 계산으로 인한 것임을 시사했습니다. 그러나 지면 제약으로 인해 해당 게시물에서 광범위한 세부 사항을 다루지 못했습니다. 이후 다른 보안 연구자들의 통찰력 있는 분석에도 불구하고, 문제의 근본 원인인 정밀도 손실은 여전히 밝혀지지 않았습니다.

흥미롭게도, 며칠 후 상황은 더욱 복잡해졌습니다. 2023년 11월 30일, 관계자들과 여러 차례 논의를 거친 후, 공격자는 외부에서 보기에 도발적으로 가득 찬 메시지를 전송하며 완전한 통제권을 요구했습니다. 그것을 제쳐두고, 공격자는 또한 중요한 정보를 공개했습니다: 문제는 실제로 정밀도 손실과 관련이 있다는 것으로, 아래 그림에서 확인할 수 있습니다. 이 공개는 우리의 조사에 대한 증거를 강화합니다. 따라서 우리의 목표는 이 보고서에서 포괄적인 분석을 제시하는 것입니다.

핵심 요약 (TL;DR)

  • 우리의 조사에 따르면, 근본적인 문제는 KyberSwap의 재투자 프로세스 중 잘못된 반올림 방향에서 비롯됩니다. 이는 이후 부적절한 틱 계산으로 이어지고 최종적으로 이중 유동성 계산을 초래합니다.

  • 이 사건은 DeFi 프로토콜 내 정밀도 손실 문제의 복잡하고 은밀한 특성을 부각시키며, 전체 커뮤니티에 상당한 도전을 제시합니다.

  • 이러한 공격의 빈도는 사전 위협 예방 조치의 중요성을 강력히 상기시켜 주며, 이는 향후 손실을 줄이는 데 크게 기여할 수 있습니다.

이후 섹션에서는 먼저 KyberSwap에 대한 중요한 배경 정보를 제공한 다음, 취약점 및 관련 공격에 대한 심층 분석을 수행할 것입니다.

0x1 배경

KyberSwap[1]은 탈중앙화 자동화 마켓 메이커(CLAMM) 플랫폼입니다. 집중 유동성의 시장 수요를 충족하기 위해, KyberSwap Elastic[3]은 Uniswap V3[2]를 기반으로 출시되었으며, 유동성 제공 수익의 자동 복리를 가능하게 하는 재투자 곡선을 포함한 여러 개선 사항이 포함되어 있습니다.

0x1.1 틱과 제곱근 가격

Uniswap V3와 유사한 CLAMM에서의 틱(Tick)은 LP가 전체 범위 대신 고정된 범위 내에서 유동성을 제공할 수 있도록 가격을 이산적인 방식으로 표시하는 데 사용됩니다(따라서 "집중"이라는 용어가 사용됨)[4].

LP가 맞춤형 가격 구간으로 유동성 포지션을 지정할 수 있도록 하기 위해, 프로토콜은 다양한 가격 포인트에 걸쳐 집계된 유동성을 추적하는 방법이 필요했습니다. Uniswap V3는 가능한 가격 공간을 이산적인 "틱"으로 분할함으로써 이를 달성했으며, LP는 임의의 두 틱 사이에 유동성을 제공할 수 있습니다.

[5]에 따르면, 유동성은 임의의 두 틱(인접할 필요 없음) 사이의 범위, 즉 틱 인덱스 쌍(하한 틱과 상한 틱)에 배치될 수 있습니다. 구체적으로, 각 틱의 가격(정수 인덱스 i에서)은 다음과 같이 정의됩니다:

실제로는 제곱근 가격(sqrtP 또는 sqrtPrice로 표시)이 사용됩니다:

현재 제곱근 가격을 기반으로 현재 틱을 계산하는 것도 가능합니다:

유동성 L과 함께 제곱근 가격을 사용하는 것은 동시 변경을 피하는 실용적인 방법입니다. 구체적으로, 가격은 틱 내에서 스왑할 때 변경되고, 유동성은 틱을 교차하거나 유동성을 민팅하거나 소각할 때 변경됩니다. 더 자세한 설명은 Uniswap V3의 백서[5]를 참조하십시오.

분명히, 특정 틱에 대해 단 하나의 제곱근 가격만 계산되지만, 여러 제곱근 가격이 동일한 틱을 가리킬 수 있습니다.

0x1.2 재투자 곡선

Uniswap V3 기반의 CLAMM은 LP 수수료의 풀 활용도와 재투자에 필요한 상당한 가스 수수료 문제를 겪고 있습니다. 따라서 KyberSwap은 이 문제를 해결하기 위해 재투자 곡선[6]을 채택했습니다:

재투자 곡선은 집중 유동성 모델에서 달리 활용되지 않는 LP 수수료를 기본적으로 재투자한다는 단일 목적으로 설계되었습니다. 이는 집중 유동성 포지션의 LP 수수료가 가스 비용이나 수동 관리 오버헤드 없이 자동으로 복리 계산됨을 의미했습니다. 또한, LP는 언제든지 자동 복리 계산된 수수료 수익을 별도로 수집하는 옵션을 여전히 보유합니다.

재투자 곡선의 핵심은 각 스왑에서 수집된 수수료가 무한한 범위 내에서 재투자 유동성으로서 풀에 추가 유동성으로 누적된다는 것입니다. 재투자 토큰은 LP에게 민팅되며, 누적된 재투자 유동성은 그에 따라 LP에게 할당됩니다. 또한, 재투자 유동성은 스왑 및 가격 계산 프로세스에도 참여합니다.

정확히 말하면, 상수 곱 공식 대신:

수수료는 각 스왑에서 ΔL로 누적됩니다:

ΔL의 계산은 다음과 같이 단순화될 수 있습니다(가격 편차가 임계값보다 낮다는 가정 하에):

그런 다음, 스왑 금액과 최종 가격은 수정된 상수 곱 공식에서 도출될 수 있습니다:

위에서 소개한 계산에 해당하는 코드는 해당 의 다음 코드 스니펫에서 computeSwapStep 함수에 표시됩니다.

재투자 유동성으로 인해 이 함수의 liquidity는 두 구성 요소의 합임을 주목해야 합니다: 기본 유동성을 위한 baseL과 재투자를 위해 누적된 유동성을 위한 reinvestL.

0x1.3 KyberSwap에서의 스왑

Uniswap V3에서 스왑의 제어 흐름은 다음과 같이 묘사될 수 있습니다[5]:

이에 따라, 앞서 논의한 KyberSwap 풀의 swap 함수 구현은 아래 다이어그램으로 추상화될 수 있습니다:

틱 계산과 관련된 중요한 로직은 파란색 직사각형으로 강조 표시된 스와핑 while 루프 내에 있습니다. 구체적으로, 주요 로직은 computeSwapStep 함수와 _updateLiquidityAndCrossTick 함수를 포함합니다. 전자는 주어진 스왑의 입력 및 출력 금액, nextSqrtP와 같은 주요 상태를 계산하고, 후자는 ***틱 교차(cross-tick)***가 발생할 때의 경우를 처리합니다.

전통적으로, 가격이 상승하면 틱이 오른쪽/위로 이동한다고 하고, 그렇지 않으면 틱이 왼쪽/아래로 이동한다고 합니다.

나중에 논의될 취약점을 더 잘 이해하기 위해서는, 다음 그림에 나와 있는 computeSwapStep 함수의 관련 코드 로직을 살펴보는 것이 필수적입니다:

먼저, 50번째 줄부터 57번째 줄까지, calcReachAmount 함수가 호출되어 targetSqrtP(다음 틱 또는 사용자 지정 목표 가격)에 도달하는 데 필요한 입력 토큰의 양을 계산합니다.

다음으로, 59번째 줄과 62번째 줄 사이에서, 틱을 교차해야 하는지 여부를 결정하는 테스트가 수행됩니다.

구체적으로, 정확한 입력 스왑(공격에 사용된 경우)에서 사용된 금액(usedAmount)이 사용자가 지정한 금액(specifiedAmount)보다 많으면, 틱을 교차하지 않아야 함을 의미하며, nextSqrtP는 증분 유동성(deltaL, 즉 델타 유동성)에서 도출되어야 합니다.

  • 이후, 70번째 줄에서 79번째 줄 사이에서, ΔL(deltaL)은 estimateIncrementalLiquidity 함수를 사용하여 입력 금액, 현재 유동성 및 가격에서 도출됩니다. 마지막으로, 스왑 후 최종 가격 nextSqrtPcalcFinalPrice 함수를 사용하여 deltaL, 입력 금액, 현재 가격 및 유동성을 기반으로 계산됩니다.

반대로, 필요한 금액이 사용자 지정 금액보다 적으면(nextSqrtP > 0을 의미), deltaL은 현재 및 목표 sqrtP를 사용하여 계산되고, nextSqrtP는 다음 틱의 sqrtP가 됩니다. 이 분기는 공격에서 사용되지 않으므로 세부 사항은 생략합니다.

위에서 설명한 단계들을 통해, 틱이 교차되지 않은 경우 computeSwapStep이 반환하는 nextSqrtP는 다음 틱의 sqrtP보다 크지 않아야 함이 명확합니다. 그러나 유동성(기본 유동성 및 델타 유동성)에 대한 가격 의존성과 정밀도 손실로 인해, 공격자는 틱이 교차되지 않은 상태에서 nextSqrtP를 더 크게 조작할 수 있습니다.

0x2 취약점 분석

근본 원인은 SwapMath 계약(computeSwapStep 함수에 의해 호출됨)의 델타 유동성 계산(즉, estimateIncrementalLiquidity 함수) 내에서 잘못된 반올림 방향으로 인한 결함 있는 틱 계산에 있습니다. 이는 결국 이후의 틱 계산에 부적절하게 영향을 미칩니다.

흥미롭게도, 188번째 줄의 주석(파란색 직사각형으로 강조 표시)을 살펴보면, deltaLnextSqrtP를 내림하기 위해 올림되도록 의도되어 있음을 알 수 있습니다. 그러나 189번째 줄에서 mulDivFloor 함수의 사용으로 인해 deltaL은 잘못되게 내림됩니다. 결과적으로, nextSqrtP는 부정확하게 올림됩니다.

0x3 공격 분석

공격자들은 여러 공격 트랜잭션을 시작했으며, 각 트랜잭션은 여러 풀을 고갈시켰습니다. 간단하게 설명하기 위해, 다음 논의는 공격 트랜잭션 내의 첫 번째 공격을 기반으로 합니다.

핵심 공격 로직은 다음 여섯 단계로 구성됩니다:

  1. AAVE에서 플래시 론을 통해 2,000 WETH 차용.

  2. 피해자 풀 0xfd7b에서 6.850 WETH를 6.371 frxETH로 스왑. 이 단계는 현재 틱과 currentSqrtP를 현재 유동성이 없는 위치로 밀어넣는 데 사용됩니다.

  • currentSqrtP는 공격자에 의해 임의로 선택된 것으로 보이며, 스왑은 이 가격에서 정확히 멈춥니다.
  • 이 단계 후 기본 유동성(baseL)은 0이지만, 재투자 유동성(reinvestL)은 0이 아닙니다.
  1. 풀에 유동성을 추가한 다음 유동성의 일부를 제거. 이 단계는 범위와 총 유동성을 원하는 양으로 제어하는 데 사용됩니다.
  • 틱 범위는 currentSqrtP를 기반으로 선택됩니다.
  • 공격에 필요한 유동성은 틱 범위에서 도출될 수 있지만, 해당 계산 로직은 추가 탐구가 필요합니다.
  1. 풀에서 387.170 WETH를 0.06 frxETH로 스왑. 이 단계는 현재 틱을 조작하여 **nextTick == currentTick**이 되도록 하는 데 사용됩니다.
  • 입력 금액은 유동성과 currentSqrtP를 기반으로 선택됩니다.
  1. 풀에서 0.06 frxETH를 396.244 WETH로 스왑. 스왑 방향은 이전 단계와 반대입니다. 이 단계에서는 유동성이 이중으로 계산되어 스왑이 수익성 있게 되고 결과적으로 풀을 고갈시킵니다.

  2. 플래시 론 상환, 6.364 WETH 및 1.117 frxETH 수확.

분명히, 마지막 두 스왑(4단계와 5단계)은 틱 계산을 조작하고 스왑을 수익성 있게 만들어 풀을 고갈시키는 핵심 공격 단계입니다. 다음 하위 섹션에서 세부 사항을 살펴보겠습니다.

3단계는 유동성을 조작하는 데 중요하다는 점에 유의해야 합니다. 반올림 연산을 통해 정밀한 틱 조작이 필요하기 때문에, 직접 유동성을 추가하는 것만으로는 목표를 달성하기 어렵습니다. 유동성 제거는 공격자가 원하는 대로 범위 내의 유동성을 정밀하게 제어하기 위한 것입니다.

0x3.1 4단계: 현재 틱과 currentSqrtP 조작

이전 단계(1단계와 2단계) 후, 공격자는 조작을 위한 틱 범위와 유동성을 준비했습니다. 구체적으로:

  • currentSqrtP는 원하는 위치에 있습니다
  • 현재 틱 = 110,909이고 다음 틱 = 111,310으로, currentSqrtP를 둘러싸고 있습니다

이 단계는 WETH를 frxETH로 스왑합니다. computeSwapStep 함수에서 다음과 같은 실행 추적이 있습니다:

위 그림에서 볼 수 있듯이, 목표(즉, 다음 틱)에 도달하는 데 필요한 금액은 calcReachAmount 함수를 호출하여 계산됩니다:

  • usedAmount = calcReachAmount(liquidity, currentSqrtP, targetSqrtP)

이 계산은 스왑 전에 도출될 수 있음에 주목하십시오. specifiedAmount를 신중하게 선택함으로써(usedAmount = specifiedAmount + 1), 공격자는 목표(즉, 다음 틱 111,310)에 도달하지 않도록 스왑을 제어했으며, 그 결과 nextSqrtP = 0이 됩니다.

이 상황에서, 틱이 교차되지 않았기 때문에, nextSqrtP(즉, 최종 가격)는 델타 유동성(스왑 수수료로 누적)에서 도출되어야 합니다.

먼저, 수수료에서 증분 유동성 deltaL이 다음과 같이 계산됩니다:

  • deltaL = estimateIncrementalLiquidity(absDelta, currentSqrtP)

그런 다음 최종 가격 nextSqrtP:

  • nextSqrtP = calcFinalPrice(absDelta, liquidity, deltaL, currentSqrtP)

이전 섹션에서 논의된 반올림 방향 오류를 다시 살펴보면, 여기서 deltaL은 잘못되게 내림되어 nextSqrtP가 올림됩니다. 구체적으로, 이 경우 동일한 absDelta(387,170,294,533,119,999,999)를 기반으로, 반올림 방향이 다르기 때문에 계산 결과가 달라집니다:

따라서, 4단계의 틱 조작 후, 현재 상태는 다음과 같이 요약됩니다:

  • currentSqrtP는 20,693,058,119,558,072,255,665,971,001,964로, 틱 111,310에서의 sqrtP(틱 111,310에서의 sqrtP = 20,693,058,119,558,072,255,662,180,724,088)보다 약간 큽니다.
  • 현재 틱 = 111,310이고 다음 틱 = 111,310

위 그림에서 볼 수 있듯이, 4단계의 스왑은 풀로 하여금 틱 111,310이 교차되지 않았다고 교묘하게 속입니다. 그러나 실제로는 currentSqrtP가 틱 111,310의 sqrtP보다 큽니다.

0x3.2 5단계: 유동성 이중 계산

4단계의 조작을 기반으로, 5단계의 공격 로직은 상당히 간단합니다. 이 단계에서 공격자는 frxETH에서 WETH로의 역방향 스왑을 조율했으며, 이는 틱과 currentSqrtP를 왼쪽으로 이동시킵니다. 구체적으로, computeSwapStep 함수는 루프 내에서 두 번 호출되며, 이는 예상치 못한 방식으로 이중 유동성 계산[7]을 유발하고 결과적으로 추가 이익을 창출합니다.

위의 추적에서 볼 수 있듯이:

  • computeSwapStep 함수의 첫 번째 호출에서, currentSqrtP는 틱 111,310의 sqrtP로 이동했습니다. 이것은 실제로 틱 111,310에 도달하기 위해 frxETH 3 wei만 사용하는 아주 작은 스왑입니다. 이후, _updateLiquidityAndCrossTick 함수 내에서, 현재 틱은 틱 111,310을 교차해야 합니다(왼쪽/아래 방향으로 이동), 비록 4단계에서 오른쪽/위 방향으로 틱 111,310을 실제로 통과하지 않았음에도 불구하고. 이로 인해 틱 111,310의 유동성이 두 번 계산됩니다.

  • computeSwapStep 함수의 두 번째 호출에서, 이전의 유동성 이중 계산은 추가 이익을 창출할 잠재력으로 이어질 수 있습니다. 구체적으로, 이 유동성 이중 계산을 활용하여, 마지막 단계의 스왑 가격이 왜곡되어 더 많은 양의 WETH가 스왑되어 나오고, 이로써 이익이 발생합니다.

0x4 공격 및 이익 요약

이 글을 작성하는 시점에서, 우리는 이더리움, Optimism, Polygon, Arbitrum, Avalanche 및 Base를 포함한 다양한 체인에서 여러 공격을 관찰했으며, $48M을 초과하는 손실을 초래했습니다. 이 공격들은 다음과 같이 서로 다른 공격자들에 의해 시작되었습니다:

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

0x5 결론

결론적으로, 이것은 부적절한 반올림 로직에서 비롯된 미묘한 취약점입니다. 익스플로잇은 믿을 수 없을 정도로 정교합니다. 실제로, 올해 우리는 정밀도 손실 문제와 관련된 일련의 보안 사고들을 관찰했으며, 이는 커뮤니티에 상당한 도전을 제시하고 있습니다.

다시 한번, 이러한 지속적인 공격들은 잠재적인 손실을 효과적으로 완화하는 데 도움이 될 수 있는 전략인 사전 위협 예방의 중요성을 보여줍니다.

참조

[1] https://docs.kyberswap.com/

[2] https://blog.uniswap.org/uniswap-v3

[3] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic

[4] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic/concepts/tick-range-mechanism

[5] https://uniswap.org/whitepaper-v3.pdf

[6] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic/concepts/reinvestment-curve

[7] https://100proof.org/kyberswap-post-mortem.html

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