Am 23. November 2023 beobachteten wir eine Reihe von Angriffen auf KyberSwap. Diese Angriffe führten zu einem Gesamtschaden von über 48 Mio. US-Dollar. Unsere erste Analyse deutete darauf hin, dass der Exploit auf Tick-Manipulation und doppelte Liquiditätszählung zurückzuführen war. Aufgrund von Platzbeschränkungen können wir jedoch nicht auf die ausführlichen Details in diesem Beitrag eingehen. Trotz anschließender aufschlussreicher Analysen anderer Sicherheitsexperten blieb die Grundursache des Problems – Präzisionsverlust – unbeleuchtet.
Interessanterweise verdichtete sich die Handlung mehrere Tage später. Am 30. November 2023, nach mehreren Diskussionsrunden mit den Beamten, sandte der Angreifer eine Nachricht, die nach außen hin provokativ und nach vollständiger Kontrolle verlangend erschien. Davon abgesehen enthüllte der Angreifer auch eine entscheidende Information: Das Problem hängt tatsächlich mit Präzisionsverlust zusammen, wie die nachstehende Abbildung zeigt. Diese Enthüllung stärkt die Beweise für unsere Untersuchung. Daher ist es unser Ziel, in diesem Bericht eine umfassende Analyse zu präsentieren.

Wichtigste Erkenntnisse (TL;DR)
-
Unsere Untersuchung zeigt, dass das grundlegende Problem aus falscher Rundungsrichtung während des Reinvestitionsprozesses von KyberSwap resultiert. Dies führt anschließend zu einer fehlerhaften Tick-Berechnung und schließlich zur doppelten Liquiditätszählung.
-
Dieser Vorfall unterstreicht die komplexe und heimtückische Natur von Präzisionsverlustproblemen innerhalb von DeFi-Protokollen und stellt eine erhebliche Herausforderung für die gesamte Gemeinschaft dar.
-
Die Häufigkeit dieser Angriffe dient als eindringliche Erinnerung an die kritische Notwendigkeit proaktiver Bedrohungsvorbeugungsmaßnahmen, die erheblich dazu beitragen könnten, zukünftige Verluste zu reduzieren.
In den folgenden Abschnitten werden wir zunächst einige wichtige Hintergrundinformationen zu KyberSwap geben. Anschließend werden wir eine eingehende Analyse der Schwachstelle und des damit verbundenen Angriffs durchführen.
0x1 Hintergrund
KyberSwap[1] ist eine dezentrale Automated Market Maker (CLAMM)-Plattform. Um der Marktnachfrage nach konzentrierter Liquidität gerecht zu werden, wurde KyberSwap Elastic[3] auf Basis von Uniswap V3[2] gestartet, mit mehreren Verbesserungen, einschließlich der Reinvestitionskurve, um das automatische Compounding der Erträge aus der Liquiditätsbereitstellung zu ermöglichen.
0x1.1 Tick und Quadratwurzelpreis
Tick in Uniswap V3-ähnlichen CLAMMs wird verwendet, um den Preis diskret zu markieren, so dass LPs Liquidität innerhalb eines festen Bereichs anstelle des gesamten Bereichs (daher der Begriff "konzentriert") bereitstellen können)[4].
Um LPs die Festlegung von Liquiditätspositionen mit kundenspezifischen Preisintervallen zu ermöglichen, benötigte das Protokoll eine Möglichkeit, die aggregierte Liquidität über verschiedene Preispunkte hinweg zu verfolgen. Uniswap V3 erreichte dies durch die Partitionierung des Raums möglicher Preise in diskrete „Ticks“, wobei LPs zwischen beliebigen zwei Ticks Liquidität beisteuern konnten.
Laut [5] kann Liquidität in einem Bereich zwischen zwei beliebigen Ticks (die nicht nebeneinander liegen müssen) platziert werden, d. h. ein Paar von Tick-Indizes (ein unterer Tick und ein oberer Tick). Konkret ist der Preis jedes Ticks (bei einem ganzzahligen Index i) wie folgt definiert:
In der Praxis wird die Quadratwurzel des Preises (bezeichnet als sqrtP oder sqrtPrice) verwendet:
Es ist auch möglich, den aktuellen Tick basierend auf dem aktuellen Quadratwurzelpreis zu berechnen:
Die Verwendung des Quadratwurzelpreises zusammen mit der Liquidität L ist eine praktische Methode, um gleichzeitige Änderungen zu vermeiden. Insbesondere ändert sich der Preis beim Tauschen innerhalb eines Ticks; die Liquidität ändert sich beim Überqueren eines Ticks oder beim Minten oder Verbrennen von Liquidität. Eine detailliertere Erklärung finden Sie im Whitepaper von Uniswap V3[5].
Offensichtlich können, obwohl für einen gegebenen Tick nur ein einziger Quadratwurzelpreis berechnet wird, mehrere Quadratwurzelpreise auf denselben Tick verweisen.
0x1.2 Reinvestitionskurve
Uniswap V3-basierte CLAMMs leiden unter der Poolauslastung von LP-Gebühren und erheblichen Gasgebühren, die für die Reinvestition erforderlich sind. Daher hat KyberSwap die Reinvestitionskurve[6] übernommen, um das Problem zu lösen:
Die Reinvestitionskurve wurde mit dem alleinigen Zweck entwickelt, die sonst ungenutzten LP-Gebühren im konzentrierten Liquiditätsmodell nativ zu reinvestieren. Das bedeutete, dass LP-Gebühren für konzentrierte Liquiditätspositionen automatisch und ohne Gas oder manuellen Verwaltungsaufwand aufgestockt wurden. Darüber hinaus haben LPs immer noch die Möglichkeit, ihre automatisch aufgestockten Gebühreneinnahmen jederzeit separat abzuholen.
Der Schlüssel zur Reinvestitionskurve ist, dass die in jedem Tausch gesammelten Gebühren als zusätzliche Liquidität im Pool als Reinvestitionsliquidität im unendlichen Bereich angesammelt werden. Die Reinvestitionstoken werden an die LPs gemintet und die angesammelte Reinvestitionsliquidität wird entsprechend den LPs zugewiesen. Darüber hinaus nimmt die Reinvestitionsliquidität auch am Tausch- und Preisberechnungsprozess teil.
Genauer gesagt, anstelle der konstanten Produktformel:
werden die Gebühren in jedem Tausch in ΔL angesammelt:
Die Berechnung von ΔL kann vereinfacht werden (unter der Annahme, dass die Preisabweichung niedriger als ein Schwellenwert ist):
Dann können der Tauschbetrag und der Endpreis aus der modifizierten konstanten Produktformel abgeleitet werden:
Der entsprechende Code für die oben eingeführten Berechnungen ist in der Funktion computeSwapStep im folgenden Code-Snippet des entsprechenden Pools zu sehen.

Es ist zu beachten, dass aufgrund der Reinvestitionsliquidität die liquidity in dieser Funktion eine Summe aus zwei Komponenten ist: baseL für die Basisliquidität und reinvestL für die angesammelte Liquidität für die Reinvestition.
0x1.3 Swap in KyberSwap
Der Kontrollfluss eines Swaps in Uniswap V3 kann wie folgt dargestellt werden[5]:

Entsprechend kann die Implementierung der swap-Funktion des zuvor diskutierten KyberSwap-Pools als das folgende Diagramm abstrahiert werden:

Die entscheidende Logik, die die Tick-Berechnung betrifft, befindet sich innerhalb der Swap-While-Schleife, wie im blauen Rechteck hervorgehoben. Insbesondere beinhaltet die Hauptlogik die Funktion computeSwapStep und die Funktion _updateLiquidityAndCrossTick. Erstere berechnet wichtige Zustände wie Eingabe- und Ausgabemengen für den gegebenen Swap und nextSqrtP, während letztere Fälle behandelt, in denen ein Tick-Übergang auftritt.
Traditionell bezeichnen wir, wenn der Preis steigt, dies als Verschiebung des Ticks nach rechts/oben; andernfalls sagen wir, der Tick bewegt sich nach links/unten.
Um die später zu diskutierende Schwachstelle besser zu verstehen, ist es unerlässlich, die relevante Code-Logik der Funktion computeSwapStep zu untersuchen, wie in der folgenden Abbildung dargestellt:

Zunächst werden in den Zeilen 50 bis 57 die calcReachAmount-Funktion aufgerufen, um die Menge des benötigten Eingabetokens zur Erreichung des targetSqrtP (nächster Tick oder benutzerdefinierter Zielpreis) zu berechnen.
Als nächstes wird zwischen den Zeilen 59 und 62 ein Test durchgeführt, um festzustellen, ob der Tick überquert werden soll oder nicht.
Insbesondere wenn die verwendete Menge (usedAmount) größer ist als die vom Benutzer angegebene Menge (specifiedAmount) beim exakten Eingabe-Swap (der im Angriff verwendete Fall), bedeutet dies, dass der Tick nicht überquert werden sollte und der nextSqrtP aus der inkrementellen Liquidität (deltaL, d. h. der Delta-Liquidität) abgeleitet werden muss.
- Anschließend werden in den Zeilen 70 bis 79 die ΔL (
deltaL) aus der Eingabemenge, der aktuellen Liquidität und dem Preis unter Verwendung der FunktionestimateIncrementalLiquidityabgeleitet. Schließlich wird der Endpreis nach dem SwapnextSqrtPbasierend auf demdeltaL, der Eingabemenge, dem aktuellen Preis und der Liquidität unter Verwendung der FunktioncalcFinalPriceberechnet.
Umgekehrt, wenn die benötigte Menge kleiner ist als die vom Benutzer angegebene Menge (was bedeutet, dass nextSqrtP > 0 ist), wird deltaL unter Verwendung des aktuellen und des Ziel-sqrtP berechnet, und der nextSqrtP ist der sqrtP des nächsten Ticks. Die Details werden weggelassen, da dieser Zweig im Angriff nicht verwendet wird.
Die oben genannten Schritte verdeutlichen, dass, wenn der Tick nicht überquert wird, der von computeSwapStep zurückgegebene nextSqrtP nicht größer sein sollte als der sqrtP des nächsten Ticks. Aufgrund der Abhängigkeit des Preises von der Liquidität (Basisliquidität und Delta-Liquidität) und des Präzisionsverlusts ist es dem Angreifer jedoch möglich, den nextSqrtP so zu manipulieren, dass er größer wird, während der Tick nicht überquert wird.
0x2 Schwachstellenanalyse
Die Grundursache liegt in der fehlerhaften Tick-Berechnung, die durch die falsche Rundungsrichtung innerhalb der Delta-Liquiditätsberechnung (d. h. der Funktion estimateIncrementalLiquidity) des SwapMath-Vertrags (der von der Funktion computeSwapStep aufgerufen wird) verursacht wird. Dies beeinträchtigt wiederum die spätere Tick-Berechnung fehlerhaft.

Interessanterweise stellen wir bei der Untersuchung des Kommentars in Zeile 188 (hervorgehoben durch das blaue Rechteck) fest, dass deltaL aufgerundet werden soll, um den nextSqrtP abzurunden. Aufgrund der Verwendung der Funktion mulDivFloor in Zeile 189 wird deltaL jedoch versehentlich abgerundet. Folglich wird nextSqrtP falsch aufgerundet.
0x3 Angriffsanalyse
Die Angreifer initiierten mehrere Angriffstransaktionen, wobei jede Transaktion mehrere Pools leerte. Der Einfachheit halber basiert die folgende Diskussion auf dem ersten Angriff innerhalb der Angriffstransaktion.
Die Kernangriffslogik besteht aus den folgenden sechs Schritten:
-
Ausleihen von 2.000 WETH über einen Flash-Loan von AAVE.
-
Tausch von 6,850 WETH gegen 6,371 frxETH im Opferpool 0xfd7b. Dieser Schritt dient dazu, den aktuellen Tick und
currentSqrtPan eine Stelle zu verschieben, an der derzeit keine Liquidität vorhanden ist.
currentSqrtPscheint vom Angreifer zufällig gewählt zu werden, und der Tausch stoppt genau bei diesem Preis.- Die Basisliquidität (
baseL) ist nach diesem Schritt null, aber die Reinvestitionsliquidität (reinvestL) ist ungleich null.
- Hinzufügen von Liquidität zum Pool und anschließendes Entfernen eines Teils der Liquidität. Dieser Schritt dient zur Steuerung des Bereichs und der Gesamtliquidität auf einen gewünschten Betrag.
- Der Tick-Bereich wird basierend auf dem
currentSqrtPgewählt. - Die gewünschte Liquidität für den Angriff könnte aus dem Tick-Bereich abgeleitet werden, obwohl die entsprechende Berechnungslogik weiter untersucht werden muss.
- Tausch von 387,170 WETH gegen 0,06 frxETH im Pool. Dieser Schritt dient zur Manipulation des aktuellen Ticks, so dass
nextTick==currentTick.
- Die Eingabemenge wird basierend auf der Liquidität und dem
currentSqrtPgewählt.
-
Tausch von 0,06 frxETH gegen 396,244 WETH im Pool. Beachten Sie, dass die Tauschrichtung im Vergleich zum vorherigen Schritt entgegengesetzt ist. In diesem Schritt wird die Liquidität doppelt gezählt, um den Tausch profitabel zu machen und folglich den Pool zu leeren.
-
Rückzahlung des Flash-Loans und Ernte von 6,364 WETH und 1,117 frxETH.
Offensichtlich sind die letzten beiden Tauschgeschäfte (Schritt 4 und Schritt 5) die entscheidenden Angriffsschritte zur Manipulation der Tick-Berechnung und zur Profitabilität des Tauschs zur Entleerung des Pools. Wir werden uns im Folgenden mit den Details befassen.
Es ist wichtig zu beachten, dass Schritt 3 entscheidend für die Manipulation der Liquidität ist. Aufgrund der Notwendigkeit einer präzisen Tick-Manipulation durch die Rundungsoperation ist es nicht praktikabel, das Ziel durch direktes Hinzufügen von Liquidität zu erreichen. Die Liquiditätsentnahme dient der präzisen Steuerung der Liquidität im gewünschten Bereich des Angreifers.
0x3.1 Schritt 4: Manipulation des aktuellen Ticks und currentSqrtP
Nach den vorherigen Schritten (Schritt 1 und 2) hat der Angreifer den Tick-Bereich und die Liquidität für die Manipulation vorbereitet. Insbesondere:
currentSqrtPbefindet sich an einer gewünschten Stelle- aktueller Tick = 110.909 und nächster Tick = 111.310, die den
currentSqrtPumgeben
Dieser Schritt tauscht WETH gegen frxETH. In der Funktion computeSwapStep haben wir die folgende Ausführungsspur:

Wie in der obigen Abbildung gezeigt, wird die zu erreichende Menge (d. h. der nächste Tick) durch Aufruf der Funktion calcReachAmount berechnet:
usedAmount=calcReachAmount(liquidity,currentSqrtP,targetSqrtP)
Beachten Sie, dass diese Berechnung vor dem Tausch abgeleitet werden kann. Durch sorgfältige Auswahl des specifiedAmount (usedAmount = specifiedAmount + 1) hat der Angreifer den Tausch so gesteuert, dass das Ziel (d. h. der nächste Tick 111.310) nicht erreicht wird, was dazu führt, dass nextSqrtP = 0 ist.
In dieser Situation, da der Tick nicht überquert wird, muss nextSqrtP (d. h. der Endpreis) aus der Delta-Liquidität (angesammelt als Tauschgebühren) abgeleitet werden.
Zuerst wird die inkrementelle Liquidität deltaL aus den Gebühren berechnet durch:
deltaL=estimateIncrementalLiquidity(absDelta,currentSqrtP)
Dann der Endpreis nextSqrtP:
nextSqrtP=calcFinalPrice(absDelta,liquidity,deltaL,currentSqrtP)
Wenn wir auf den Fehler der Rundungsrichtung zurückkommen, der im vorherigen Abschnitt diskutiert wurde, werden hier deltaL fälschlicherweise abgerundet, was dazu führt, dass nextSqrtP aufgerundet wird. Insbesondere in diesem Fall ergeben sich bei gleicher absDelta (387.170.294.533.119.999.999) aufgrund unterschiedlicher Rundungsrichtungen unterschiedliche Berechnungsergebnisse:

Daher sind nach der Tick-Manipulation in Schritt 4 die aktuellen Zustände wie folgt zusammengefasst:
currentSqrtPist 20.693.058.119.558.072.255.665.971.001.964, geringfügig größer als dersqrtPbei Tick 111.310 (sqrtP bei 111.310 = 20.693.058.119.558.072.255.662.180.724.088).- aktueller Tick = 111.310 und nächster Tick = 111.310

Wie in der obigen Abbildung dargestellt, täuscht der Swap in Schritt 4 den Pool geschickt vor, dass Tick 111.310 nicht überquert wurde. In Wirklichkeit ist der currentSqrtP jedoch tatsächlich größer als der sqrtP von Tick 111.310.
0x3.2 Schritt 5: Doppelte Liquiditätszählung
Basierend auf der Manipulation in Schritt 4 ist die Angriffslogik in Schritt 5 recht einfach. In diesem Stadium orchestrierte der Angreifer einen umgekehrten Tausch von frxETH zu WETH, der den Tick und den currentSqrtP nach links verschieben würde. Insbesondere wird die Funktion computeSwapStep innerhalb der Schleife zweimal aufgerufen, was letztendlich auf unvorhergesehene Weise die doppelte Liquiditätszählung[7] auslöst und folglich zusätzliche Gewinne generiert.

Wie im obigen Trace gezeigt:
-
Bei der ersten Aufrufung der Funktion
computeSwapStepwurde dercurrentSqrtPauf densqrtPvon Tick 111.310 verschoben. Dies ist ein winziger Tausch, der nur 3 Wei frxETH verwendet, um tatsächlich Tick 111.310 zu erreichen. Anschließend sollte innerhalb der Funktion_updateLiquidityAndCrossTickder aktuelle Tick Tick 111.310 überqueren (sich nach links/unten bewegen), obwohl er in Schritt 4 Tick 111.310 in Richtung rechts/oben nicht wirklich durchlaufen hat. Dies führt dazu, dass die Liquidität bei Tick 111.310 zweimal gezählt wird. -
Bei der zweiten Aufrufung der Funktion
computeSwapStepkann die vorherige doppelte Zählung der Liquidität zu zusätzlichen Gewinnen führen. Insbesondere durch Ausnutzung dieser doppelten Liquiditätszählung wird der Tauschpreis im abschließenden Schritt verzerrt, was zu einem größeren WETH-Tausch führt und somit einen Gewinn generiert.
0x4 Zusammenfassung der Angriffe und Gewinne
Zum Zeitpunkt der Erstellung dieses Berichts haben wir eine Reihe von Angriffen auf verschiedenen Chains (einschließlich Ethereum, Optimism, Polygon, Arbitrum, Avalanche und Base) in freier Wildbahn beobachtet, die zu Verlusten von über 48 Mio. US-Dollar führten. Diese Angriffe wurden von verschiedenen Angreifern durchgeführt, wie folgt:
Eine vollständige Liste dieser Angriffstransaktionen wurde in einem von uns erstellten Dokument gesammelt. Bitte beziehen Sie sich für detailliertere Informationen darauf.
0x5 Schlussfolgerung
Zusammenfassend lässt sich sagen, dass es sich hier um eine subtile Schwachstelle handelt, die aus einer fehlerhaften Rundungslogik resultiert. Der Exploit ist unglaublich raffiniert. Tatsächlich haben wir in diesem Jahr eine Reihe von Sicherheitsvorfällen im Zusammenhang mit Präzisionsverlustproblemen beobachtet, die erhebliche Herausforderungen für die Gemeinschaft darstellen.
Diese kontinuierlichen Angriffe zeigen einmal mehr die Bedeutung proaktiver Bedrohungsvorbeugung, einer Strategie, die potenzielle Verluste wirksam mindern kann.
Referenz
[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



