#5 Yearn Finance Vorfall: Unsichere Arithmetik im Invarianzlöser macht seinem Namen alle Ehre

#5 Yearn Finance Vorfall: Unsichere Arithmetik im Invarianzlöser macht seinem Namen alle Ehre

Am 30. November 2025 wurde der yETH Weighted Stable Pool von Yearn Finance für über 9 Millionen US-Dollar ausgenutzt [1]. Die Hauptursachen waren unsichere Arithmetik im Invarianten-Solver _calc_supply() und ein nicht deaktivierter Bootstrap-Pfad, der die Wiederaufnahme der Initialisierungslogik ermöglichte. Der offizielle Post-Mortem-Bericht [2] listet fünf Hauptursachen auf; wir stufen diese neu als zwei Defekte (die oben genannten Schwachstellen) und zwei architektonische Vorbedingungen ein, die nur in Verbindung mit diesen Defekten ausnutzbar wurden. Andere verfügbare Analysen konzentrieren sich auf schrittweise Transaktionsdetails des Angriffs. Zwischen hochrangigen Zusammenfassungen und Transaktionsdetails bleibt eine Lücke: Warum und wie funktionierte der Angriff tatsächlich? Dieser Beitrag füllt diese Lücke und verwendet Foundry- und Python-Simulationen, um zu verfolgen, wie sich Schlüsselwerte Schritt für Schritt entwickeln und wo die Berechnungen fehlschlagen.

Diese Analyse leistet hauptsächlich die folgenden drei Beiträge:

  1. Verlustaufschlüsselung nach Schwachstelle. Die beiden Schwachstellen sind nicht voneinander abhängig: Allein unsichere Arithmetik verursachte Verluste von ca. 8,1 Mio. US-Dollar (90 % des Gesamtbetrags), während der Bootstrap-Pfad zusätzliche ca. 0,9 Mio. US-Dollar ermöglichte. Dies klärt, welche Schwachstelle primär war.
  2. Neuklassifizierung der Ursachen. Die fünf Hauptursachen des offiziellen Berichts werden besser verstanden als zwei Implementierungsfehler (die drei der fünf Punkte konsolidieren) zuzüglich zweier architektonischer Vorbedingungen, die nur in Kombination mit den Fehlern ausnutzbar wurden.
  3. Korrektur technischer Missverständnisse. Die Behauptung, dass „ein Unterlauf in der zweiten Iteration den Produktterm auf Null setzt“, ist nicht zutreffend: Unsere Simulationen zeigen, dass das Produkt durch Rundung bei der Division auf Null gesetzt wird, nicht durch Unterlauf, und der gewinnbringende Unterlauf tritt in einer völlig anderen Phase auf.

Der Rest dieses Beitrags ist wie folgt organisiert. Abschnitt 0x1 bietet Hintergrundinformationen zum Weighted Stable Pool von yETH und seinem Invarianten-Solver. Abschnitt 0x2 analysiert die beiden Hauptursachen und ihre Fehlermodi. Abschnitt 0x3 verfolgt den dreiphasigen Angriff im Detail. Abschnitt 0x4 korrigiert zwei gängige Missverständnisse mit Simulationsbeweisen. Abschnitt 0x5 schließt mit Empfehlungen.

TL;DR

Ursachen: Zwei Schwachstellen wurden ausgenutzt, jedoch mit asymmetrischem Einfluss:

  1. Unsichere Arithmetik in _calc_supply() (primär, ~8,1 Mio. US-Dollar). Die Funktion, die die yETH-Versorgung aus dem Poolzustand neu berechnet, enthält zwei arithmetische Fehler: Abrundung in unsafe_div() kann den internen Produktterm auf Null setzen, und Unterlauf in unsafe_sub() kann einen Zwischenwert auf eine enorme positive Ganzzahl umschlagen. Diese Schwachstelle allein reichte aus, um den yETH Weighted Stableswap Pool zu leeren.
  2. Nicht deaktivierter Bootstrap-Pfad (sekundär, ~0,9 Mio. US-Dollar). Der Initialisierungszweig prev_supply == 0 wurde nach der Bereitstellung nie dauerhaft gesperrt. Nachdem die erste Schwachstelle die Versorgung auf Null reduziert hatte, wurde dieser Pfad erreichbar und ermöglichte zusätzlichen Gewinn aus dem yETH/WETH Curve Pool.

Innerhalb der Schwachstelle der unsicheren Arithmetik wurde nur der Abrundungsfehler (Fehlermodus A) in Phase 2 verwendet; der Unterlauffehler (Fehlermodus B) ist vom Bootstrap-Pfad abhängig und gemeinsam ermöglichten sie Phase 3.

Der Angreifer führte eine dreiphasige Sequenz aus:

  1. Vorbereitung: Verzerren Sie die Vermögensverteilung des Pools durch wiederholte Hinzufüge-/Entnahmezyklen, wodurch extreme Ungleichgewichte bei den virtuellen Guthaben entstehen.
  2. Versorgungsmanipulation: Nutzen Sie die Abrundung in _calc_supply(), um den Produktterm auf Null zu reduzieren, und leeren Sie dann die gesamte Versorgung durch eine Reihe von Mint/Burn-Operationen. Alle LSTs des Pools wurden abgezogen und anschließend in WETH getauscht, was zu Verlusten von ca. 8,1 Mio. US-Dollar führte.
  3. Gewinnentnahme: Lösen Sie den Bootstrap-Pfad (prev_supply == 0) mit minimalen Einzahlungen aus, nutzen Sie den Unterlauf in _calc_supply(), um ca. 2,35 x 10⁵⁶ yETH zu prägen, die zum Leeren des yETH/WETH Curve Pools verwendet wurden, was zu Verlusten von ca. 0,9 Mio. US-Dollar führte.

Zwei häufige Missverständnisse, die korrigiert wurden:

  • „Die Invariante bricht, weil pow_up() und pow_down() unterschiedlich runden.“ Wir haben durch den Ersatz von pow_up() durch pow_down() in einer Foundry-Simulation verifiziert: Der Exploit funktioniert immer noch. Rundungsunterschiede sind keine Hauptursache.
  • „Ein Unterlauf in der zweiten Iteration setzt einen Zwischenterm auf Null.“ Unsere Foundry- und Python-Simulationen zeigen, dass in der zweiten Iteration kein Unterlauf auftritt. Der tatsächliche Wert ist ~1,91e19 (nicht ~1,94e18 wie behauptet), ein legitimes Ergebnis einer korrekten Subtraktion. Was das Produkt auf Null setzt, ist die anschließende Abrundung bei der Division, nicht ein Unterlauf.

0x1 Hintergrund

Zwei Pools verloren bei diesem Vorfall Vermögenswerte: der yETH Weighted Stableswap Pool (ein Yearn-Pool, der LSTs hält, ~8,1 Mio. US-Dollar Verlust) und der yETH/WETH Curve Pool (ein Curve Stableswap Pool, ~0,9 Mio. US-Dollar Verlust). Der yETH Weighted Stableswap Pool ist die Quelle der Kernschwachstelle. Dieser Abschnitt bietet Hintergrundinformationen, die zum Verständnis der Schwachstelle und des Exploits notwendig sind.

0x1.1 Virtuelle Guthaben und die Invariante

Das yETH-Protokoll ist ein Automated Market Maker (AMM) für Ethereum Liquid Staking Tokens (LSTs) [3]. Der betroffene yETH Weighted Stableswap Pool aggregiert mehrere LSTs in einem einzigen Pool: Benutzer zahlen LSTs ein und erhalten yETH als Pool-Anteile-Token.

Da jeder LST ETH repräsentiert, der über die Zeit Belohnungen erwirtschaftet, ändert sich sein Wechselkurs im Verhältnis zu Basis-ETH. Um die Buchhaltung zu vereinheitlichen, definiert der Pool für jedes Asset ein virtuelles Guthaben xix_i: On-Chain-Guthaben × Wechselkurs. Dies normalisiert alle Assets in Beacon-Chain-ETH-Einheiten. Die Summe aller virtuellen Guthaben wird mit σ=xi\sigma = \sum x_i bezeichnet.

Der Pool enthält 8 Assets (indiziert 0–7), die jeweils ein bestimmtes Gewicht wiw_i haben:

Index Asset Index Asset
0 sfrxETH 4 rETH
1 wstETH 5 apxETH
2 ETHx 6 WOETH
3 cbETH 7 mETH

Der Zustand des Pools wird durch eine gewichtete StableSwap-ähnliche Invariante gesteuert [4]:

Afn  σ+D=Afn  D+Dπ(1)\mathit{Af}^{\,n}\;\sigma + D = \mathit{Af}^{\,n}\;D + D \cdot \pi \tag{1}

wobei:

  • DD die Invariante Skala ist, die direkt der gesamten yETH-Versorgung dieses Pools entspricht. Wenn der Pool perfekt ausbalanciert ist, gilt D=σD = \sigma.
  • π\pi der gewichtete Produktterm ist, definiert als π=Dni(wixi)vi\pi = D^n \prod_{i} \left(\frac{w_i}{x_i}\right)^{v_i}, wobei wiw_i das Gewicht des Assets i und vi=winv_i = w_i \cdot n ist.
  • Af\mathit{Af} der Amplifikationsfaktor ist, ein einzelner Protokollparameter (nicht A×fA \times f). Afn\mathit{Af}^{\,n} bezeichnet diesen Faktor hoch nn, wobei nn die Anzahl der Assets ist (8 in diesem Pool). Er steuert die Kurvenform zwischen konstant-sum (nahe dem Gleichgewicht) und konstant-product (an Extremen).

Die Schlüsseleigenschaft: DD hat keine geschlossene Form. Es muss numerisch gelöst werden. Dieser Solver, _calc_supply(), ist die Stelle, an der die arithmetische Schwachstelle liegt.

0x1.2 Der Invarianten-Solver

Das Protokoll berechnet DD über eine Fixed-Point-Iteration, die auf 256 Runden begrenzt ist. Dieser Algorithmus ist im Code als _calc_supply() implementiert (Details in Abschnitt 0x2.1). Jede Runde führt drei Schritte aus:

Schritt 1: Aktualisieren Sie die Versorgungsabschätzung.

Dm+1=AfnσDmπmAfn1(2)D_{m+1} = \frac{\mathit{Af}^{\,n} \cdot \sigma - D_m \cdot \pi_m}{\mathit{Af}^{\,n} - 1} \tag{2}

Schritt 2: Aktualisieren Sie den Produktterm, um der neuen Versorgung zu entsprechen.

πm+1=πm(Dm+1Dm)n(3)\pi_{m+1} = \pi_m \cdot \left(\frac{D_{m+1}}{D_m}\right)^n \tag{3}

Schritt 3: Prüfen Sie auf Konvergenz.

Wenn Dm+1Dm<ϵ|D_{m+1} - D_{m}| < \epsilon, geben Sie DmD_{m} zurück; andernfalls wiederholen Sie ab Schritt 1.

Die Anfangswerte D0D_0, π0\pi_0 und σ\sigma beeinflussen die frühen Iterationen; obwohl sie theoretisch für die endgültige Konvergenz irrelevant sind, beeinflussen sie die Ergebnisse in der Praxis aufgrund endlicher Iterationen und Festkomma-Arithmetik.

Die Implementierung verwendet Festkomma-Ganzzahloperationen: Division rundet ab, und Subtraktion schützt nicht vor Unterlauf. Unter normalen Poolbedingungen bleiben die Zwischenwerte innerhalb sicherer Bereiche. Unter extremen Poolzuständen tun sie das nicht. Abschnitt 0x2.1 analysiert diese Fehlermodi im Detail.

0x1.3 Die drei Schnittstellen und der Invarianten-Solver

Das Protokoll stellt drei Einstiegspunkte zur Verfügung, die den Poolzustand beeinflussen, indem sie den gewichteten Produktterm π\pi (im Code als vb_prod gespeichert) aktualisieren:

Schnittstelle Was sie tut Löst _calc_supply() aus?
add_liquidity() Einzahlungen von Assets in beliebigen Verhältnissen Ja
update_rates() Aktualisierung externer Wechselkurse Ja
remove_liquidity() Abhebungen von Assets proportional zum Gewicht Nein (verwendet proportionale Skalierung)

Die Asymmetrie ist wichtig: add_liquidity() erlaubt Einzahlungen in beliebigen Verhältnissen (es kann den Pool massiv verzerren), während remove_liquidity() immer proportional abzieht. Wiederholte Zyklen von Hinzufügen/Entfernen können den Pool daher in immer unausgeglichenere Zustände überführen.

Der Mechanismus zur Aktualisierung von Raten

Wie oben erläutert, werden virtuelle Guthaben (xix_i) basierend auf den Wechselkursen von LSTs berechnet. Daher ist es wichtig, die Methode zur Aktualisierung von Raten zu verstehen.

Insbesondere können die Funktionen add_liquidity() und update_rates() Raten über die interne Funktion _update_rates() aktualisieren, während die Funktion remove_liquidity() keine Raten-Synchronisation durchführt.

  • add_liquidity() ruft _update_rates() auf, bevor kritische Operationen ausgeführt werden, um sicherzustellen, dass die Asset-Wechselkurse mit dem neuesten Stand synchronisiert sind.
  • update_rates() ermöglicht manuelle Ratenaktualisierungen.

Die Funktion _update_rates() prüft, ob die im Vertrag aufgezeichneten Wechselkurse mit den externen Wechselkursen übereinstimmen. Wenn eine Abweichung festgestellt wird, löst sie eine Neuberechnung der virtuellen Guthaben aus und aktualisiert anschließend die Invariante; andernfalls wird der Aktualisierungsprozess übersprungen.

Wie jede Schnittstelle mit π umgeht

Basierend darauf, wie sie die Invariante beeinflussen, können diese drei Funktionen in zwei Kategorien eingeteilt werden. Insbesondere erlauben add_liquidity() und update_rates() nicht-proportionale Änderungen der virtuellen Guthaben und erfordern daher eine iterative Neuberechnung des Angebots DD und des Produkts π\pi. Im Gegensatz dazu zieht remove_liquidity() Liquidität proportional ab und erfordert keine iterative Berechnung.

Der gewichtete Produktterm π\pi (im Code als vb_prod gespeichert) muss sowohl von add_liquidity() als auch von remove_liquidity() aktualisiert werden. Ähnlich wird die Summe der virtuellen Guthaben σ\sigma als vb_sum gespeichert.

Die Basisformel zur Berechnung des Produkts von Grund auf lautet:

π=i(Dwixi)nwi(4)\pi = \prod_{i} \left(\frac{D \cdot w_i}{x_i}\right)^{n \cdot w_i} \tag{4}

wobei DD das Angebot, wiw_i das Gewicht von Asset ii, xix_i sein virtuelles Guthaben (im Code als vb[i] gespeichert) und nn die Anzahl der Assets ist. Diese Form ist algebraisch äquivalent zur Definition in Abschnitt 0x1.1, wobei DnD^n in das Produkt verteilt wird.

  1. add_liquidity() hat zwei Pfade (Code siehe Abschnitt 0x2.2):
    • Bootstrap-Pfad (wenn prev_supply == 0): Berechnet vb_prod von Grund auf mit Gleichung (4). Dieser Pfad, der nach der Bereitstellung zugänglich bleibt, ist die in Abschnitt 0x2.2 diskutierte Zustandsverwaltungs-Schwachstelle.
    • Normaler Pfad (wenn prev_supply > 0): Der Berechnungsprozess ist in zwei Schritte unterteilt:
      • a) Verwendet eine inkrementelle Aktualisierung basierend auf dem Verhältnis der alten zu den neuen virtuellen Guthaben:

        πgescha¨tzt=πi=0n1(xixi)win(5)\pi_{\text{geschätzt}} = \pi \cdot \prod_{i=0}^{n-1} \left(\frac{x_i}{x_i'}\right)^{w_i \cdot n} \tag{5}

        wobei xix_i und xix_i' die virtuellen Guthaben vor und nach der Einzahlung sind.
      • b) Kalibriert iterativ den genauen Wert, indem _calc_supply() mit dieser Schätzung als Eingabe aufgerufen wird, wodurch die Invariante DD und der genaue Wert von π\pi neu berechnet werden.
  2. update_rates() wird aufgerufen, wenn sich die Wechselkurse ändern, was dazu führt, dass die virtuellen Guthaben der entsprechenden Assets aktualisiert werden. Sein nachfolgender Berechnungsfluss folgt dem normalen Pfad von add_liquidity(), d. h. die Invariante wird iterativ neu berechnet. Zusätzlich prägt oder verbrennt der Vertrag basierend auf dem neu berechneten Angebot yETH, um sicherzustellen, dass das Liquiditätsangebot mit dem aktualisierten virtuellen Guthabenstand konsistent bleibt.
  3. remove_liquidity() berechnet vb_prod immer von Grund auf mit Gleichung (4), nachdem jedes virtuelle Guthaben proportional reduziert wurde.

0x2 Ursachenanalyse

Zwei Schwachstellen wurden ausgenutzt, mit unterschiedlichen Rollen und Auswirkungen. Die primäre Ursache war ein Berechnungsfehler im Invarianten-Solver _calc_supply(), der zwei Fehlermodi hatte: (A) Abrundung konnte das Produkt auf Null setzen, wodurch die Invariante zu einem konstanten Summenmodell degenerierte und zu übermäßiger LP-Prägung (Angebotsinflation) führte; und (B) ein Unterlaufzustand konnte ebenfalls das Angebot aufblähen. Nur Fehlermodus A wurde in Phase 2 (~8,1 Mio. US-Dollar) verwendet. Fehlermodus B war vom sekundären Schwachpunkt abhängig.

Die sekundäre Ursache war ein Zustandsverwaltungsfehler: Der Initialisierungszweig des Pools blieb zugänglich. Nachdem Phase 2 das Angebot auf Null reduziert hatte, kombinierte Fehlermodus B mit dem Bootstrap-Pfad, um zusätzliche Verluste von ~0,9 Mio. US-Dollar zu ermöglichen (Phase 3).

0x2.1 Unsichere Arithmetik in `_calc_supply()` (Primär)

Abbildung 2 bildet die Implementierung von _calc_supply() auf das mathematische Verfahren aus Abschnitt 0x1.2 ab und annotiert die beiden unten analysierten arithmetischen Fehlerstellen:

Die Code-Variablen entsprechen den mathematischen Termen wie folgt:

Code-Variable Mathematische Rolle
s Aktueller Versorgungsabschätzung DmD_m
r Produktterm πm\pi_m
sp Nächste Versorgungsabschätzung Dm+1D_{m+1}
l Zählerkonstante: Afnσ\mathit{Af}^{\,n} \cdot \sigma
d Nennerkonstante: Afn1\mathit{Af}^{\,n} - 1

Die kritischen Ausdrücke sind:

sp = unsafe_div(unsafe_sub(l, unsafe_mul(s, r)), d)   # Schritt 1: D[m+1]
r  = unsafe_div(unsafe_mul(r, sp), s)                 # Schritt 2: π-Update (pro Asset)

Zwei arithmetische Fehlermodi existieren innerhalb dieser Funktion, die auf unterschiedliche Zeilen abzielen und unterschiedliche Auswirkungen haben. Beide erfordern, dass der Pool in einem extremen Zustand ist, um ausgelöst zu werden.

Unter normalen Bedingungen verhält sich die Iteration korrekt: l - s * r ist ein moderater positiver Wert, und die Iteration konvergiert in wenigen Runden.

1. Fehlermodus A: Abrundung setzt das Produkt auf Null

In Schritt 2 wird das Produkt pro Asset aktualisiert als:

r = unsafe_div(unsafe_mul(r, sp), s)   # r = r * sp / s

Da unsafe_div() Ganzzahl-Division durchführt, rundet es immer ab. Wenn der Pool stark unausgeglichen ist und sp viel kleiner ist als s (wie es nach einer manipulierten großen Einzahlung geschieht), kann der Zähler r * sp kleiner als der Nenner s werden. Die Ganzzahl-Division ergibt dann r = 0.

Sobald r Null ist, bleibt es in allen nachfolgenden Iterationen Null. Der Produktterm π\pi ist dauerhaft kollabiert.

Eine häufige Fehlzuschreibung behauptet, dieser Fehler rühre von einer Rundungsinkonsistenz zwischen pow_up() und pow_down() her. Abschnitt 0x4 liefert Beweise dafür, dass dies falsch ist.

2. Fehlermodus B: Unterlauf bläht das Angebot auf

In Schritt 1 wird die neue Versorgungsabschätzung berechnet als:

sp = unsafe_div(unsafe_sub(l, unsafe_mul(s, r)), d)   # sp = (l - s*r) / d

Die Subtraktion l - s*r AfnσDmπm\mathit{Af}^{\,n} \cdot \sigma - D_m \cdot \pi_m in Gleichung 2. Unter normalen Bedingungen ist dies positiv. Wenn der Pool jedoch einen degenerierten Zustand mit Nullversorgung erreicht, berechnet der Initialisierungszweig in add_liquidity() (detailliert in Abschnitt 0x2.2) den Produktterm von Grund auf neu, und die relativen Größen können sich umkehren.

Insbesondere wenn add_liquidity() auf einem Pool mit Nullversorgung mit winzigen Beträgen aufgerufen wird, ruft der Initialisierungszweig _calc_vb_prod_sum() auf, um neue Werte mit Gleichung (4) (Abschnitt 0x1.3) zu berechnen. Bei winzigen Einzahlungen ist vb_sum winzig (z. B. 16), aber die Division durch fast Null-Guthaben und die Potenzierung zu hohen Potenzen verstärkt das Produkt zu einem unverhältnismäßig großen Wert (z. B. ~9,13e20). Wenn s * r größer als l ist, ergibt die Subtraktion ein mathematisch negatives Ergebnis.

Da unsafe_sub() die Subtraktion in ungeprüfter uint256-Arithmetik durchführt, schlägt ein negatives Ergebnis zu einer enormen positiven Ganzzahl (nahe 22562^{256}) um. Dieser umschlagene Wert breitet sich durch die Division und nachfolgende Iterationen aus und liefert eine absurd große Versorgungsabschätzung, die das Protokoll dann als echtes yETH-Token prägt.

Eine übliche Behauptung besagt, dass ein solcher Unterlauf in der zweiten Iteration eines bestimmten Versorgungsmanipulationsschritts auftritt. Abschnitt 0x4 zeigt, dass diese Behauptung falsch ist: Der tatsächliche Unterlauf, der das Angebot aufbläht, tritt in einem völlig anderen Kontext auf (Phase 3 des Angriffs).

3. Wie diese Fehler den Angriff ermöglichen

Diese beiden Fehlermodi wirken in verschiedenen Phasen des Exploits mit unterschiedlichen Gewinnbeiträgen:

  • Fehlermodus A (Phase 2, ~8,1 Mio. US-Dollar): Wenn der Angreifer in einen stark unausgeglichenen Pool einzahlt, wird der Produktterm auf Null gesetzt, wodurch _calc_supply() eine aufgeblähte Versorgung zurückgibt. Das Protokoll prägt übermäßig yETH für den Angreifer. Dieser Fehler allein, ohne Beteiligung des Bootstrap-Pfades, ermöglichte es dem Angreifer, den yETH Weighted Stableswap Pool seiner LST-Assets zu leeren.
  • Fehlermodus B (Phase 3, ~0,9 Mio. US-Dollar): Nachdem die Versorgung auf Null reduziert wurde, ermöglicht der Bootstrap-Pfad durch minimale Einzahlungen die Neuberechnung eines großen Produktterms, was zu einer Subtraktion führt, die einen Unterlauf verursacht. Das Protokoll prägt eine astronomisch große Menge yETH, die der Angreifer verwendet, um den separaten yETH/WETH Curve Pool zu leeren.

Die Abhängigkeit ist unidirektional: Fehlermodus A ist unabhängig ausnutzbar und verursachte 90 % der Verluste, während Fehlermodus B Fehlermodus A erfordert, um zuerst die Versorgung auf Null zu reduzieren.

0x2.2 Nicht deaktivierter Bootstrap-Pfad (Sekundär)

Die Funktion add_liquidity() enthält einen Zweig für die erstmalige Einzahlung in den Pool:

Die Logik kann wie folgt abstrahiert werden:

if prev_supply == 0:
    # Bootstrap-Pfad — berechnet vb_prod und vb_sum von Grund auf
    vb_prod, vb_sum = _calc_vb_prod_sum(balances, rates, weights, ...)
    supply = vb_sum
else:
    # Normaler Pfad — verwendet gespeichertes vb_prod, führt inkrementelle Prüfungen durch
    ...

# Nach beiden Zweigen aufgerufen, mit prev_supply == 0 als Flag
supply, vb_prod = _calc_supply(num_assets, supply, amplification, vb_prod, vb_sum, prev_supply == 0)

Wenn prev_supply == 0, umgeht die Funktion den gespeicherten Zustand und berechnet vb_prod und vb_sum von Grund auf neu über _calc_vb_prod_sum() mit Gleichung (4) (Abschnitt 0x1.3). Dieser Bootstrap-Zweig war für den einmaligen Gebrauch während der Pool-Initialisierung vorgesehen, wurde aber nach der ersten Einzahlung nie dauerhaft gesperrt.

Wenn die Gesamtversorgung auf Null reduziert werden kann (durch beliebige Kombinationen von Burns und Abhebungen), wird der Zweig wieder erreichbar. Ein Angreifer, der diesen Pfad erneut betritt, kontrolliert die Anfangsbedingungen, die an _calc_supply() übergeben werden, und kann möglicherweise die oben beschriebenen arithmetischen Fehler unter Parametern auslösen, die unter normalen Poolbedingungen niemals auftreten würden.

Dies ist ein bekanntes Schwachstellenmuster. Im August 2023 hing der Balancer V2 Vorfall ebenfalls davon ab, das Angebot auf Null zu reduzieren, um interne Raten zurückzusetzen, was es dem Angreifer ermöglichte, die Initialisierungslogik zu künstlich günstigen Parametern erneut aufzurufen [6]. Ob ein bereitgestellter Pool in seinen Anfangszustand zurückgeführt werden kann und welche Invarianten dabei gelten, ist eine Frage, die Protokolldesigner explizit berücksichtigen müssen.


0x3 Angriffsanalyse

Der Exploit entfaltet sich über eine koordinierte Abfolge von Angriffstransaktionen [5], die in drei Phasen organisiert ist. Jede Phase baut auf dem Zustand auf, der von der vorherigen Phase etabliert wurde.

0x3.1 Phase 1: Verzerrung des Pools (Vorbereitung)

Ziel: Extreme Ungleichgewichte bei den virtuellen Guthaben zwischen den Assets erzeugen.

Die folgende Abbildung zeigt die Transaktionsspur für diese Phase (der Flash-Loan-Schritt ist aus Platzgründen weggelassen):

Der Angreifer leiht sich zunächst große Mengen an LST-Assets über Flash-Loans von Balancer und Aave, insbesondere 5.500e18 wstETH, 3.100e18 WETH, 1.800e18 rETH, 2.000e18 ETHx und 200e18 cbETH.

Als Nächstes tauscht der Angreifer ungefähr 800e18 WETH gegen etwa 416e18 yETH im yETH/WETH Curve Pool und nutzt dann das erworbene yETH, um Liquidität aus dem Pool zu entziehen.

Die Kernmanipulation nutzt die in Abschnitt 0x1 (Hintergrund) beschriebene Schnittstellenasymmetrie aus: add_liquidity() erlaubt Einzahlungen in beliebigen Verhältnissen, während remove_liquidity() Assets proportional nach Pool-Gewichten abzieht (im roten Rechteck in der obigen Abbildung hervorgehoben). Durch wiederholtes Zyklieren von Hinzufügen → Entfernen, Einzahlung nur ausgewählter Assets und gleichzeitiges proportionale Abziehen aller Assets treibt der Angreifer den Pool fortschreitend in einen stark unausgeglichenen Zustand:

Asset Gewicht Vorher Nachher Änderung
0 (sfrxETH) 20% 628.097.482.908.289.585.170 684.908.495.923.316.419.717 +9,04%
1 (wstETH) 20% 376.569.216.105.249.117.091 684.906.088.027.654.432.883 +81,88%
2 (ETHx) 10% 187.473.530.249.048.974.586 410.441.661.092.336.995.160 +118,93%
3 (cbETH) 10% 267.387.722.745.796.900.349 3.532.430.695.689.175.233 -98,68%
4 (rETH) 10% 201.828.029.369.446.137.136 410.441.659.865.060.509.563 +103,36%
5 (apxETH) 25% 753.792.636.209.697.936.333 549.134.446.963.315.842.411 -27,15%
6 (WOETH) 2,5% 49.640.000.870.620.479.267 655.788.758.768.556.847 -98,68%
7 (mETH) 2,5% 47.667.894.211.903.277.629 629.735.467.970.876.930 -98,68%

Assets 3 (cbETH), 6 (WOETH) und 7 (mETH) wurden um über 98 % geleert. Diese Ungleichheit entzieht nicht direkt Gewinne. Sie schafft die numerischen Voraussetzungen für die nächste Phase.

0x3.2 Phase 2: Reduzierung des Angebots auf Null (~8,1 Mio. US-Dollar)

Ziel: Das Invariantenprodukt auf Null reduzieren, dann die yETH-Versorgung auf Null leeren. Diese Phase nutzt nur die primäre Schwachstelle (unsichere Arithmetik) und verursachte ~90 % der Gesamtverluste.

Diese Phase verwendet einen sich wiederholenden Fünf-Schritte-Zyklus, der dreimal ausgeführt wird:

  1. Produkt verfälschen durch add_liquidity();
  2. Voraussetzung für Korrektur schaffen durch add_liquidity();
  3. Produkt zurücksetzen durch remove_liquidity() mit 0 yETH;
  4. Angebot korrigieren durch update_rates();
  5. Assets abziehen durch remove_liquidity().

Die folgende Abbildung zeigt die Transaktionsspur, wo drei Wiederholungen des Fünf-Schritte-Zyklus deutlich sichtbar sind:

1. Produkt verfälschen durch `add_liquidity()`

Der Angreifer zahlt hohe Gewichte Assets (Indizes 0, 1, 2, 4, 5: sfrxETH, wstETH, ETHx, rETH, apxETH) ein, jeweils etwa das Dreifache ihres aktuellen virtuellen Guthabens.

add_liquidity() schätzt das neue Produkt mit der inkrementellen Aktualisierung in Gleichung (5) (Abschnitt 0x1.3). Da xixix_i' \gg x_i für hochgewichtete Assets gilt, sind die Verhältnisse (xi/xi)(x_i / x_i') alles Brüche weit unter 1, potenziert mit hohen Potenzen. Dies reduziert πneu\pi_{\text{neu}} von ~42e18 auf ~0,00353e18, ein fast Null geschätztes Produkt.

Dieses winzige Produkt gelangt in _calc_supply(). In der Iteration trifft die Produktaktualisierung r = r * sp / s auf die Abrundungsbedingung, die in Abschnitt 0x2 (Ursachenanalyse) beschrieben ist: der Zähler fällt unter den Nenner, und die Ganzzahl-Division setzt r auf Null. Die Funktion gibt ein Nullprodukt und ein aufgeblähtes Angebot (≈vb_sum) zurück, was dazu führt, dass das Protokoll yETH übermäßig prägt.

2. Voraussetzung für Korrektur schaffen durch `add_liquidity()`

Der Angreifer fügt einseitig Liquidität für Asset-Index 3 (cbETH, ein erschöpftes, niedrig gewichtetes Asset) hinzu und zahlt ca. das 6,5-fache des aktuellen Pool-Guthabens des Assets ein. Dies bringt nur wenige yETH-Token, rebalanciert aber den Pool genug, dass die nächste Iteration nicht stark schwankt.

Ohne diesen Schritt würde selbst nach dem Zurücksetzen des Produkts auf einen Wert ungleich Null in Schritt 3 die Iteration in Schritt 4 aufgrund heftiger Schwankungen aufgrund des extremen Ungleichgewichts immer noch ein Nullprodukt liefern. Unsere Foundry-Simulation bestätigt dies: Das Überspringen von Schritt 2 führt zum Fehlschlag der Korrektur in Schritt 4.

3. Produkt zurücksetzen durch `remove_liquidity()` mit 0 yETH

Der Angreifer ruft remove_liquidity() mit Betrag 0 auf. Es werden keine Token abgezogen, aber die Funktion berechnet vb_prod neu aus dem aktuellen Poolzustand mit Gleichung (4) (Abschnitt 0x1.3). Da die virtuellen Guthaben ungleich Null sind, ergibt dies ein Produkt ungleich Null (~9,09e19) und überschreibt den verfälschten Nullwert.

4. Angebot korrigieren durch `update_rates()`

Der Angreifer ruft update_rates() für Asset-Index 6 (WOETH) oder 7 (mETH) auf. Wenn sich der Wechselkurs seit der letzten Aktualisierung geändert hat, löst die Funktion _calc_supply() mit dem wiederhergestellten (ungleich Null) Produkt aus. Dieses Mal konvergiert die Iteration korrekt und ergibt eine viel niedrigere Angebotsschätzung als die aktuelle aufgeblähte. Die Differenz wird aus dem yETH-Staking-Vertrag verbrannt. Laut dem offiziellen Post-Mortem [2] handelt es sich hierbei um Protokolleigenes Liquidität (POL), was bedeutet, dass die Burns die Position des Protokolls und nicht die Bestände des Angreifers reduzieren. Diese Asymmetrie ist entscheidend: Jeder Zyklus reduziert das Gesamtangebot, während das yETH-Guthaben des Angreifers intakt bleibt.

Die Ratenabweichung selbst ist keine Gewinnquelle; sie dient ausschließlich als Auslösemechanismus. Von den drei Pool-Schnittstellen rufen nur add_liquidity() und update_rates() _calc_supply() auf; remove_liquidity() verwendet proportionale Skalierung und tut dies nicht. Nachdem Schritt 3 ein Produkt ungleich Null wiederhergestellt hat, muss der Angreifer _calc_supply() auslösen, ohne zusätzliche Assets einzuzahlen. Der Aufruf von update_rates() mit einer veralteten Rate erreicht genau dies: die Ratenänderung löst die Neuberechnung des Angebots ohne Kosten für den Angreifer aus.

Dies erklärt einen subtilen Aspekt des Angriffs: Während der Vorbereitungsphase (Phase 1) vermied der Angreifer bewusst die Hinzufügung von Liquidität für WOETH und mETH. Wenn diese Raten während add_liquidity() aktualisiert worden wären, gäbe es keine Ratenabweichung, und update_rates() würde in diesem Schritt _calc_supply() nicht auslösen.

5. Assets abziehen durch `remove_liquidity()`

Am Ende jedes Zyklus zieht der Angreifer Assets über remove_liquidity() ab.

Wie Gewinne erzielt werden

Der Gewinnmechanismus funktioniert wie folgt: In Schritt 1 zahlt der Angreifer LSTs ein und erhält übermäßig geprägtes yETH (aufgrund des verfälschten Produkts). In Schritt 4, wenn das Angebot korrigiert wird, wird das überschüssige yETH aus dem POL (Staking-Vertrag) verbrannt, nicht aus dem Angreifer. In Schritt 5 zieht der Angreifer LSTs proportional zu seinen yETH-Beständen ab. Da POL den Burn absorbierte, während das yETH-Guthaben des Angreifers intakt blieb, zieht der Angreifer am Ende mehr LSTs ab, als er eingezahlt hat. Diese Differenz, über drei Zyklen extrahiert, summiert sich auf ca. 8,1 Mio. US-Dollar.

Zweck der Neuberechnung (Rebase)

Die Spur (zwischen dem ersten und dem zweiten Zyklus) zeigt auch einen Aufruf von OETHVaultProxy.rebase(), der eine OETH-Neuberechnung auslöst: das Guthaben von OETH, das vom WOETH-Vertrag gehalten wird, erhöht sich, wodurch der effektive Wechselkurs von WOETH steigt. Diese „gerettete“ Ratenabweichung macht Schritt 4 des zweiten Zyklus erneut möglich: Wenn update_rates() schließlich aufgerufen wird, erkennt es die Abweichung und löst _calc_supply() aus.

Leeren bis auf Null

Nachdem dieser Fünf-Schritte-Zyklus dreimal wiederholt wurde, hat der Angreifer das Gesamtangebot des Pools auf weniger als das von ihm gehaltene yETH reduziert. Ein letzter Aufruf von remove_liquidity() mit dem verbleibenden Angebot leert es auf NULL.

Der Pool hält nun Null Angebot, Null Produkt und Null vb_sum. Dieser degenerative Zustand verletzt die implizite Designannahme, dass ein Pool mit vorherigen Einzahlungen niemals in seinen uninitialisierten Zustand zurückkehren würde.

0x3.3 Phase 3: Ausnutzung von Null-Angebot für zusätzlichen Gewinn (~0,9 Mio. US-Dollar)

Ziel: Eine enorme Menge yETH aus dem degenerierten Poolzustand prägen, dann gegen echte Assets tauschen. Diese Phase nutzt die abhängige Kombination der sekundären Schwachstelle (nicht deaktivierter Bootstrap-Pfad) und Fehlermodus B (Unterlauf) aus, die zusammen ~10 % der Gesamtverluste ausmachen.

1. Prägen durch Unterlauf

Bei einem Gesamtangebot von Null ruft der Angreifer add_liquidity() mit minimalen Beträgen auf (Guthaben [1, 1, 1, 1, 1, 1, 1, 9]).

Da prev_supply == 0, tritt der Code in den Bootstrap-Pfad ein, der in Abschnitt 0x2 (Ursachenanalyse) beschrieben ist: Er umgeht den gespeicherten Zustand und berechnet vb_prod und vb_sum von Grund auf neu über _calc_vb_prod_sum(), übergibt diese dann an _calc_supply(). Dies ist die zweite Schwachstelle in Aktion: Der Angreifer hat den Pool in seinen uninitialisierten Zustand zurückgeführt und die Anfangsbedingungen kontrolliert, die an den Solver übergeben werden.

Bei allen virtuellen Guthaben auf minimalem Niveau (Wechselkurse nahe 1e18) sind die berechneten Werte:

  • vb_sum = 16
  • vb_prod ≈ 9,13e20
  • _supply = vb_sum = 16

Innerhalb von _calc_supply() werden die Variablen wie folgt initialisiert:

  • l = _amplification * _vb_sum ≈ 4,5e20 × 16 ≈ 7,2e21
  • d = _amplification - PRECISION4,49e20
  • s = _supply = 16
  • r = _vb_prod9,13e20

Nun die Subtraktion l - s * r:

7.2×102116×9.13×1020=7.2×10211.46×10227.4×10217.2 \times 10^{21} - 16 \times 9.13 \times 10^{20} = 7.2 \times 10^{21} - 1.46 \times 10^{22} \approx -7.4 \times 10^{21}

Dies ist negativ. In ungeprüfter uint256-Arithmetik schlägt unsafe_sub dies auf eine etwa 22567.4×10212^{256} - 7.4 \times 10^{21}, einen astronomisch großen Wert, um. Nach der Division durch d (~4,49e20) beträgt die resultierende Versorgungsabschätzung ~2,35e56, und das Protokoll prägt diesen gesamten Betrag für den Angreifer. Dieser Unterlauf ist nur möglich, weil das Gesamtangebot in Phase 2 auf Null reduziert wurde; in jedem nicht-degenerierten Poolzustand gilt l > s * r, und die Subtraktion ist sicher.

2. Tausch gegen echte Assets

Der Angreifer tauscht einen Teil des übermäßig geprägten yETH gegen ~1.097e18 WETH im yETH-WETH Curve Pool und leert damit dessen WETH-Reserven. Nach Abzug der 800e18 WETH, die in Phase 1 ausgegeben wurden, betrug der Netto-Gewinn ca. 0,9 Mio. US-Dollar.

Zusammen mit den ~8,1 Mio. US-Dollar an LST-Assets, die während Phase 2 entnommen wurden, erzielt der Angreifer insgesamt etwa 9 Millionen US-Dollar Gewinn nach Rückzahlung der Flash-Loans.

Eine detaillierte Analyse des Geldflusses, einschließlich der Herkunft und des Ziels der Gelder, wurde in anderen veröffentlichten Analysen (z. B. [2]) behandelt und liegt außerhalb des Rahmens dieses Artikels.


0x4 Korrektur von Missverständnissen

Die meisten veröffentlichten Analysen dieses Vorfalls konzentrieren sich auf die arithmetischen Symptome, ohne vollständig zu erklären, wie der Angreifer die Voraussetzungen schafft. Zwei spezifische Behauptungen verdienen eine Korrektur.

0x4.1 Behauptung: „Rundungsinkonsistenz zwischen `pow_up()` und `pow_down()` verfälscht die Invariante“

Eine gängige Interpretation führt die Hauptursache auf die Verwendung von pow_up() in einigen Code-Pfaden und pow_down() in anderen zurück und argumentiert, dass die gerichtete Inkonsistenz ausnutzbare Unstimmigkeiten einführt.

Wir haben dies direkt getestet: Wir haben den Vertrag so modifiziert, dass pow_down() einheitlich verwendet wird (alle pow_up()-Aufrufe ersetzt) und die vollständige Angriffs-Simulation in Foundry erneut ausgeführt. Der Exploit war identisch erfolgreich. Das Produkt bricht immer noch auf Null zusammen, das Angebot wird immer noch geleert, und der Unterlauf erzeugt immer noch eine aufgeblähte Prägung.

Die Rundung, die den Zustand des Nullprodukts ermöglicht, ist die Ganzzahl-Division mit Abrundung in r = unsafe_div(unsafe_mul(r, sp), s) innerhalb der Iterationsschleife, nicht die Richtung der Rundung in den Potenzfunktionen, die zur Schätzung der anfänglichen Produktwerte verwendet werden.

0x4.2 Behauptung: „Unterlauf in der zweiten Iteration setzt den Zwischenterm auf Null“

Eine weit verbreitete Erklärung besagt, dass während der zweiten Iteration von _calc_supply() ein Unterlauf in unsafe_sub sp ≈ 1,94e18 ergibt, was dann dazu führt, dass r auf Null abgerundet wird.

Wir haben die exakten Zwischenwerte sowohl mit Foundry (On-Chain-Wiederholung) als auch mit Python (mathematische Verifizierung) reproduziert. Die Foundry-Simulation verfolgt _calc_supply() Iteration für Iteration:

======= _calc_supply iteration 0 =======
  l = 4905875511098192451202650000000000000000
  s = 2514373972590845290489        ← anfängliches Angebot
  r = 3538247433646816               ← anfängliches Produkt (sehr klein)
  d = 4490000000000000000000

  sp = (l - s*r) / d ≈ 1.093e22     ← neues Angebot springt ~4x
  new r ≈ 4.49e22                    ← Produkt explodiert dramatisch

======= _calc_supply iteration 1 =======
  s = 10926206313726454855296        ← aus vorherigem sp
  r = 44892226765713223838396        ← aus vorheriger innerer Schleife

  sp = 19113493328251743069          ← ≈ 1.91e19, legitimerweise klein
  new r = 0                          ← rundet auf Null!

Die kritische Beobachtung: In Iteration 1 ergibt sich sp zu ~1,91e19. Dies ist ein legitim kleiner positiver Wert, kein Artefakt eines Unterlaufs. Die Subtraktion l - s*r ergibt ein kleines positives Ergebnis, da die gewichtete Summe l und der Angebot-Produkt-Term s*r in dieser Iteration in ihrer Größenordnung nahe beieinander liegen.

Was das Produkt auf Null setzt, ist, was danach geschieht: Die innere Schleife berechnet r = r * sp / s, wobei sp (~1,91e19) weitaus kleiner ist als s (~1,09e22). Der Zähler r * sp fällt unter den Nenner s, und die Ganzzahl-Division setzt das Ergebnis auf Null.

Wir haben dies unabhängig in Python verifiziert, indem wir die gleichen Werte mit Ganzzahlen beliebiger Genauigkeit berechnet und bestätigt haben, dass die Subtraktion keinen Unterlauf verursacht:

Das Produkt wird durch Rundung bei der Division auf Null gesetzt, nicht durch Unterlauf bei der Subtraktion. Der unsafe_sub-Unterlauf, der das Angebot aufbläht, tritt in einem völlig anderen Kontext auf: Phase 3 des Angriffs, wenn minimale Liquidität in einen Pool eingezahlt wird, der auf Null Angebot geleert wurde.


0x5 Schlussfolgerung

Der yETH-Exploit umfasste zwei Schwachstellen mit asymmetrischem Einfluss. Unsichere Arithmetik in _calc_supply() war die primäre Ursache: Ihr Abrundungsfehler (Fehlermodus A) ermöglichte unabhängig Verluste von ca. 8,1 Mio. US-Dollar allein durch Phase 2. Der nicht deaktivierte Bootstrap-Pfad war eine sekundäre Schwachstelle; kombiniert mit dem Unterlauffehler (Fehlermodus B) ermöglichte er zusätzliche Verluste von ca. 0,9 Mio. US-Dollar in Phase 3, jedoch erst, nachdem Phase 2 das Angebot bereits auf Null geleert hatte. Diese Verlustaufschlüsselung unterscheidet die vorliegende Analyse von anderen veröffentlichten Berichten, die die Gewinne von Phase 2 und Phase 3 nicht trennen.

Der offizielle Post-Mortem [2] identifiziert fünf Ursachen. Wir stufen diese neu als zwei Defekte (unsichere Arithmetik konsolidiert die offiziellen Punkte #1 und #5; nicht deaktivierter Bootstrap-Pfad als #4) und zwei architektonische Vorbedingungen (#2 asymmetrische Π-Behandlung; #3 POL-fähiger Null-Angebot-Zustand) ein. Der Unterschied: Defekte sind Implementierungsfehler, die gegen die Designabsicht verstoßen (der Solver sollte keine Nullprodukte erzeugen oder unterlaufen), während Vorbedingungen Designentscheidungen sind, die wie vorgesehen funktionieren, aber eine ausnutzbare Angriffsfläche schaffen, wenn sie mit Defekten kombiniert werden.

Empfehlungen

  • Geprüfte Arithmetik in Invarianten-Solvern. Verwenden Sie safe_div und safe_sub mit expliziter Rücksetzung bei Unterlauf/Überlauf, auch auf Kosten der Gas-Effizienz. Der Solver läuft höchstens 256 Iterationen, und der Gas-Overhead ist im Vergleich zum Sicherheitsrisiko vernachlässigbar.
  • Grenzwerte für Zwischenwerte. Validieren Sie, dass der Produktterm zwischen den Iterationen innerhalb eines sinnvollen Bereichs bleibt. Ein Produkt, das auf Null fällt, oder eine Versorgungsabschätzung, die zwischen den Iterationen um Größenordnungen zunimmt, signalisiert einen degenerierten Zustand.
  • Grenzen für Ungleichgewichte. Erzwingen Sie eine maximale Abweichung zwischen dem virtuellen Guthaben eines beliebigen Assets und seinem Ziel-gewichts-proportionalen Guthaben. Dies würde verhindern, dass Phase 1 die Voraussetzungen schafft.
  • Monotonieprüfungen der Invariante. Überprüfen Sie nach Rückgabe von _calc_supply(), ob die neue Versorgung mit der Änderungsrichtung konsistent ist (Hinzufügen von Liquidität sollte niemals die Versorgung verringern, Ratenaktualisierungen sollten keine 10-fachen Änderungen ergeben usw.).
  • Initialisierungszweige dauerhaft deaktivieren. Nach der ersten Einzahlung in den Pool sperren Sie den prev_supply == 0-Bootstrap-Zweig, sodass er nicht erneut aufgerufen werden kann. Dies würde Phase 3 vollständig verhindern.
  • Null-Angebot-Zustände verhindern. Stellen Sie sicher, dass protokollweite Burns (aus POL oder Staking-Verträgen) das Gesamtangebot nicht auf Null reduzieren können, während der Pool nicht-Null-Guthaben enthält. Eine Mindestgrenze für das Angebot würde den Übergang in den degenerierten Zustand blockieren, der die erneute Aufnahme des Bootstrap-Pfades ermöglicht.
  • Echtzeit-Anomalieerkennung. Überwachen Sie auf anormale Zustandsübergänge (wie das Fallen von Produkttermen auf Null, Angebotsänderungen um Größenordnungen oder wiederholte Hinzufüge-/Entnahmezyklen in kurzen Zeiträumen) und lösen Sie Alarme oder Notbremsen aus, bevor sich Verluste summieren.

Referenzen

  1. Yearn Finance Vorfallankündigung
  2. Yearn Security Post-Mortem
  3. yETH Dokumentation
  4. yETH Whitepaper: Ableitung
  5. Angriffstransaktion im BlockSec Explorer
  6. BlockSec: Analyse des Balancer Boosted Pool Vorfalls (August 2023)

Über BlockSec

BlockSec ist ein Anbieter von Full-Stack-Blockchain-Sicherheit und Krypto-Compliance. Wir entwickeln Produkte und Dienstleistungen, die Kunden bei der Code-Prüfung (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 im gesamten Lebenszyklus von Protokollen und Plattformen unterstützen.

BlockSec hat mehrere Blockchain-Sicherheitsartikel auf renommierten Konferenzen veröffentlicht, mehrere Zero-Day-Angriffe auf DeFi-Anwendungen gemeldet, mehrere Hacks blockiert, um mehr als 20 Millionen Dollar zu retten, und Milliarden von Kryptowährungen gesichert.

Sign up for the latest updates