#8 Bunni-Vorfall: Wiederholte Kleinstabhebungen summieren Rundungsfehler zu 8,4 Mio. $ Abfluss

#8 Bunni-Vorfall: Wiederholte Kleinstabhebungen summieren Rundungsfehler zu 8,4 Mio. $ Abfluss

Am 2. September 2025 erlitt das Bunni V2-Protokoll einen ausgeklügelten Exploit [1]. Ein Angreifer nutzte eine kritische Schwachstelle in seinem Liquiditätsbuchhaltungsmechanismus aus, um rund 8,4 Millionen US-Dollar aus zwei Liquiditätspools zu entziehen: dem USDC/USDT-Pool auf Ethereum [2] und dem weETH/ETH-Pool auf Unichain [3].

Ursache war ein Rundungsfehler bei der Aktualisierung der ungenutzten Pool-Salden bei Liquiditätsentzug durch das Protokoll. Dieser Fehler führte zu einer erheblichen Unterbewertung der Gesamtliquidität im Vertrag und schuf eine ausnutzbare Diskrepanz zwischen der theoretischen und der tatsächlichen Liquidität. Der Angreifer führte daraufhin einen präzisen Sandwich-Angriff durch, um von dieser Diskrepanz zu profitieren.

Dieser Vorfall führte direkt zu schweren finanziellen Verlusten für das Bunni-Protokoll, das daraufhin am 23. Oktober 2025 Insolvenz anmeldete [4].

Hintergrund

Bunni V2 ist ein Automated Market Maker (AMM)-Protokoll, das auf Uniswap V4 aufbaut. Es implementiert seine Kernlogik über den Hook-Mechanismus und führt Innovationen auf dem konzentrierten Liquiditätsalgorithmus von Uniswap V3 ein, um Liquiditätsanbietern (LPs) eine verbesserte Kapitaleffizienz zu bieten [5].

Insbesondere verbessert das Protokoll die LP-Renditen hauptsächlich durch eine Rehypothekarisierungsfunktion und einen Rebalancing-Mechanismus. Ersteres weist Liquidität externen ertragsgenerierenden Protokollen zu, sichert die Basisliquidität und erzielt zusätzliche externe Erträge. Letzteres optimiert kontinuierlich die Verteilung der Liquidität über Preisbereiche und erhöht die aktive Nutzung von Kapital zur Steigerung der Gebühreneinnahmen. Diese beiden Mechanismen bilden die Kerninnovationen des Protokolls auf dem grundlegenden Modell der konzentrierten Liquidität.

Rehypothekarisierung

Zur Steigerung der Renditen für Liquiditätsanbieter setzt Bunni V2 auf eine Rehypothekarisierungsstrategie. Diese Strategie weist Gelder verschiedenen Positionen zu:

  • rawBalance: Ein Teil der Reserven des Pools für einen Token wird direkt im contract PoolManager von Uniswap V4 gespeichert. Dies dient als sofort verfügbare Liquidität zur Ermöglichung von Swaps.
  • reserves: Der Rest wird in einem bestimmten ERC4626-Tresor hinterlegt. Dies ermöglicht es den Nutzern, zusätzliche externe Erträge auf diese Vermögenswerte zu erzielen.

Daher definieren sich die Gesamtvermögenswerte für einen Pool als: Pool-Assets = rawBalance + zugrunde liegender Betrag von reserves.

Rebalancing

Zur Steigerung der Gebühreneinnahmen implementiert Bunni V2 einen Rebalancing-Mechanismus, der den zeitgewichteten Durchschnittspreis überwacht. Wenn die Preisänderung einen Schwellenwert überschreitet, wird die Liquidität gemäß der Liquiditätsverteilungsfunktion (LDF) über verschiedene Preisbereiche umverteilt.

Diese Neuzuweisung kann das von der LDF geforderte Token-Verhältnis ändern und einen Überschuss bei einem Token hinterlassen. Dieser Überschuss wird als ungenutztes Gleichgewicht definiert.

Somit wird die Liquidität in zwei Teile geteilt:

  • Active Balance: Der von der LDF zugewiesene Teil, der an der Liquiditätsberechnung teilnimmt.
  • Idle Balance: Der Überschuss, der nicht für aktive Liquidität verwendet wird.

Daher gilt: Pool-Assets = Active Balance + Idle Balance.

Schlüsselfunktionen: Liquiditätsberechnung und -entzug

Dieser Angriff nutzt zwei kritische Funktionen aus: queryLDF() und withdraw(). Die Funktion queryLDF() berechnet die Liquidität des Pools für Swaps, während die Funktion withdraw() Benutzern erlaubt, eine proportionale Liquidität zu entziehen.

Funktionen `queryLDF()`

Aufgrund der Rehypothekarisierungsstrategie ist die Menge der zugrunde liegenden Vermögenswerte dynamisch, und Bunni V2 speichert keinen festen "Gesamtliquiditäts"-Wert. Stattdessen stellt das Protokoll die Funktion queryLDF() bereit, um die Echtzeit-Liquidität bei einem Swap abzurufen [6]. Der Ausführungsprozess dieser Funktion besteht aus den folgenden vier Schritten:

  1. Abfrage der Liquiditätsdichte:

    1. Aufruf der Liquiditätsdichtefunktion ldf.query(), die die Liquiditätsdichte außerhalb des aktuellen Preis-Tick-Bereichs ermittelt.

    2. Aufruf von LiquidityAmounts.getAmountsForLiquidity(), um die Dichte innerhalb des aktuellen Tick-Bereichs zu ermitteln.

    3. Berechnung der gesamten Liquiditätsdichte von Token0 und Token1 in beide Richtungen, bezeichnet als totalDensity0 und totalDensity1.

    Es ist bemerkenswert, dass die Funktion LiquidityAmounts.getAmountsForLiquidity() aufrundet, um sicherzustellen, dass die berechneten Token-Mengen konservativ nicht geringer sind als die theoretischen Werte.

  2. Berechnung des verfügbaren Saldos

    Die für die Liquiditätsberechnungen verwendeten verfügbaren Salden werden als balance0 und balance1 bezeichnet. Der ungenutzte Saldo wird vom Gesamtbestand des entsprechenden Tokens abgezogen, wobei Gelder ausgeschlossen werden, die nicht an der Liquiditätsberechnung teilnehmen.

    In diesem Angriff, bei dem die ungenutzten Gelder des Pools aus token0 bestanden, lauten die Berechnungsformeln:

    • balance0=rawBalance0+reserve0idleBalancebalance0 = rawBalance0 + reserve0 - idleBalance

    • balance1=rawBalance1+reserve1balance1 = rawBalance1 + reserve1

  3. Schätzung der effektiven Liquidität

    1. Schätzung der Liquidität, die jeder Token unterstützen kann, basierend auf seinem tatsächlich verfügbaren Saldo (balance0 oder balance1) und der berechneten Gesamtdichte (totalDensity0 oder totalDensity1).

    2. Auswahl des kleineren der beiden Schätzwerte als endgültige effektive Gesamtliquidität.

    Die Formel lautet wie folgt:

    L=min(balance0totalDensity0,balance1totalDensity1)L= min(\frac{balance0}{totalDensity0},\frac{balance1}{totalDensity1})

  4. Berechnung der aktiven Salden

    Basierend auf der ermittelten Gesamtliquidität berechnet das Protokoll die tatsächliche Menge der für den Handel verfügbaren Token. Dies wird als Active Balance definiert.

Funktion `withdraw()`

Bunni V2 stellt die Funktion withdraw() zum Entzug von Liquidität bereit. Benutzer entziehen Liquidität proportional zu ihrem Anteil an den Gesamtmitteln des Pools. Das Protokoll aktualisiert rawBalance, reserves und idleBalance im gleichen Verhältnis. Die Anpassungsformel lautet wie folgt:

(rawBalance,reserves,idleBalance)=(rawBalance,reserves,idleBalance)×(1sharestotalSupply)(rawBalance, reserves, idleBalance) \\= (rawBalance, reserves, idleBalance) \times (1-\frac{shares}{totalSupply})

Wobei:

  • shares die Anzahl der vom Benutzer entzogenen Liquiditätsanteile ist;
  • totalSupply der Gesamtanbieter von Liquiditätstoken für diesen Pool ist.

Schwachstellenanalyse

Die Schwachstelle ergibt sich aus der Berechnung des Anpassungsbetrags für den ungenutzten Saldo in der Funktion withdraw(), die eine Bodenrundung (d. h. Abrundung) verwendet. Dies führt zu einer Überschätzung des ungenutzten Saldos.

Wenn wir uns an die Formel für den verfügbaren Saldo erinnern, balance=rawBalance+reserveidlebalancebalance = rawBalance + reserve - idle balance. Eine überschätzte ungenutzte Bilanz führt direkt zu einer unterschätzten verfügbaren Bilanz (balance0), die für die Liquiditätsberechnungen verwendet wird. Folglich wird auch die geschätzte effektive Gesamtliquidität unterbewertet. Laut dem Bunni Exploit Post Mortem [7] wurde diese Rundungsrichtung bei Liquiditätsberechnungen absichtlich verwendet. Ein niedrigerer berechneter Liquiditätswert führt zu einer höheren Preisauswirkung während Swaps.

Dieses Design beruht auf einer kritischen Annahme: Das Verhältnis der Salden zwischen den beiden Token bleibt relativ ausgeglichen. Unter normalen Bedingungen mit ausreichender Liquidität sind die für jeden Token separat geschätzten Gesamtliquiditätswerte typischerweise nahe beieinander. Die Auswirkungen des Rundungsfehlers sind daher begrenzt. Wenn jedoch der verfügbare Saldo des Tokens, der einen ungenutzten Saldo trägt, extrem niedrig wird, tritt der Fehler zutage. In diesem Szenario wird der Bodenrundungsfehler erheblich verstärkt.

Der Angreifer nutzte diese Schwachstelle aus, indem er eine Reihe kleiner Abhebungen durchführte, wodurch der verfügbare Saldo von Token0 von 28 Wei auf 4 Wei abgerundet wurde. Dieser Rückgang überstieg bei weitem den Anteil der tatsächlich verbrannten Liquiditätsanteile. In der Zwischenzeit blieb der verfügbare Saldo von Token1 auf einem relativ normalen Niveau. Dieses Ungleichgewicht schuf ein erhebliches Arbitragemitarbeiterfenster. Das nächste Kapitel bietet eine detaillierte numerische Analyse.

Angriffsanalyse

Am Beispiel der Ethereum-Transaktion [2] führte der Angreifer einen dreistufigen Angriff durch:

  • In der ersten Phase manipulierte der Angreifer den Preis, um den USDC-Verfügbarkeitssaldo (Token0) erheblich zu reduzieren. Dies schuf die notwendigen Ausgangsbedingungen, um den anschließenden Rundungsfehler zu verstärken.
  • In der zweiten Phase wurde der Kern-Exploit durch eine Reihe kleiner Abhebungen durchgeführt, was dazu führte, dass das Protokoll die tatsächliche Liquidität des Pools unterschätzte.
  • In der dritten Phase führte der Angreifer zwei gerichtete Swaps durch, um die Diskrepanz zwischen der vom Protokoll unterschätzten Liquidität und der tatsächlichen Liquidität des Pools zu arbitrieren und letztendlich Gewinne zu erzielen.

Phase 1: Preismanipulation und Reduzierung des Ziel-Token-Saldos

Der Angreifer führte drei Swap-Transaktionen durch, manipulierte den Preis von USDC (Token0) relativ zu USDT (Token1) und trieb ihn von einem Anfangs-Tick = -1 zu einem Tick = 5000. Der Hauptzweck war die Reduzierung des aktiven USDC-Saldos des Pools auf ein extrem niedriges Niveau von 28 Wei. Dies schuf die notwendigen Ausgangsbedingungen, um den anschließenden Rundungsfehler in der nächsten Phase zu verstärken.

Phase 2: Ausnutzung von Abhebungen zur Verstärkung von Liquiditätsdiskrepanzen

Der Angreifer initiierte 44 kleine Abhebungen über die Funktion withdraw(). Aufgrund der Bodenrundung, die diese Funktion bei der Aktualisierung von idleBalance verwendet, wurde der ungenutzte Saldo des Protokolls überschätzt. Dies führte zu einer weiteren Unterschätzung des verfügbaren USDC-Saldos in der Funktion queryLDF(). Nach diesen wiederholten Operationen wurde der verfügbare USDC-Saldo von 28 Wei auf 4 Wei abnormal stark reduziert. Dies entsprach einer tatsächlichen Reduzierung von 85,7 %, weit über dem theoretischen Anteil, der den entzogenen Liquiditätsanteilen entsprach (d. h. 8,998105442969973e-07 %). Zu diesem Zeitpunkt war die geschätzte Liquidität aus USDC im Pool stark unterbewertet.

Phase 3: Arbitrage und Gewinnrealisierung

Der Angreifer führte dann zwei gerichtete Swaps durch, was einer Operation ähnelte, die einem Sandwich-Angriff entspricht.

Schritt 1: Der Angreifer tauschte eine große Menge USDT gegen USDC. Zu diesem Zeitpunkt war die interne Liquiditätsberechnung aufgrund des unterschätzten USDC-Saldos stark unterbewertet. Dieser große Swap trieb den Preis auf ein Extrem und verschob den Tick von 5.000 auf 839.189.

Schritt 2: Nach der extremen Preisbildung kehrte der Angreifer die Operation sofort um und tauschte einen Teil des USDC wieder in USDT. Da der Preis des Pools nun stark fehlausgerichtet war, sank der Rückgabewert der queryLDF()-Funktion für USDC-Liquiditätsdichte auf 1. Dies führte dazu, dass der anhand von USDC geschätzte Liquiditätswert größer war als der anhand von USDT geschätzte Wert.

Gemäß der Logik des Protokolls, den kleineren Wert auszuwählen, wird die Gesamtliquidität durch den USDT-Saldo bestimmt. Dies führte dazu, dass sich die berechnete Liquidität sofort von einem unterbewerteten Zustand auf ein normales Niveau zurückstellte, was zu einem plötzlichen Anstieg führte. Der Angreifer nutzte diese Verschiebung aus, tauschte eine minimale Menge USDC gegen eine große Menge USDT und schloss damit die Arbitrage ab und realisierte einen Gewinn.

Zusammenfassung

Dieser Vorfall wurde letztendlich durch Rundungsfehler bei der Anpassung von ungenutzten Salden während des Liquiditätsentzugs verursacht. Obwohl diese Bodenfunktionsgestaltung als Sicherheitsstrategie bei Liquiditätsberechnungen gedacht war, berücksichtigte sie kritische Randbedingungen nicht ausreichend. Insbesondere werden Rundungsfehler nicht-linear verstärkt, wenn die Token-Salden stark unausgeglichen sind.

Dieser Vorfall deckt die Risiken der Kopplung zwischen mehreren Modulen in komplexen DeFi-Protokollen auf. Selbst wenn die Rundungsregeln einzelner Komponenten konservativ ausgelegt sind, kann ein Mangel an konsistenter Sicherheitsvalidierung über das gesamte System hinweg zu kritischen Schwachstellen führen, die unter bestimmten Umständen ausgenutzt werden können.

Referenz

  1. https://x.com/bunni_xyz/status/1962833866277744953
  2. https://etherscan.io/tx/0x1c27c4d625429acfc0f97e466eda725fd09ebdc77550e529ba4cbdbc33beb97b
  3. https://uniscan.xyz/tx/0x4776f31156501dd456664cd3c91662ac8acc78358b9d4fd79337211eb6a1d451
  4. https://x.com/bunni_xyz/status/1981160279871558114
  5. https://docs.bunni.xyz/docs/v2/overview
  6. https://github.com/Bunniapp/bunni-v2/blob/2b303b8c1b9f8afbb169d62ba52da93d6d2171fe/src/lib/QueryLDF.sol#L40
  7. https://blog.bunni.xyz/posts/exploit-post-mortem/

Über BlockSec

BlockSec ist ein Full-Stack-Anbieter für Blockchain-Sicherheit und Krypto-Compliance. Wir entwickeln Produkte und Dienstleistungen, die Kunden bei der Durchführung von Code-Audits (einschließlich Smart Contracts, Blockchain und Wallets), der Echtzeit-Abwehr von Angriffen, der Analyse von Vorfällen, der Verfolgung illegaler Gelder und der Erfüllung von AML/CFT-Verpflichtungen über den gesamten Lebenszyklus von Protokollen und Plattformen hinweg unterstützen.

BlockSec hat mehrere Blockchain-Sicherheitsarbeiten auf renommierten Konferenzen veröffentlicht, mehrere Zero-Day-Angriffe auf DeFi-Anwendungen gemeldet, mehrere Hackerangriffe zur Rettung von mehr als 20 Millionen US-Dollar blockiert und Kryptowährungen im Wert von Milliarden gesichert.

Sign up for the latest updates