Scannez pour télécharger l’application Gate
qrCode
Autres options de téléchargement
Ne pas rappeler aujourd’hui

La magie noire de la manipulation des prix : Analyse de la vulnérabilité de calcul d'invariance de Balancer V2

Compilation : Blockchain en langage courant

Le 3 novembre 2025, les Composable Stable Pools de Balancer V2 ainsi que plusieurs projets fork sur plusieurs chaînes ont subi une attaque coordonnée, entraînant une perte totale de plus de 125 millions de dollars. BlockSec a immédiatement émis une alerte et a ensuite publié une analyse préliminaire.

C'est une attaque hautement complexe. Notre enquête montre que la cause profonde est une perte de précision dans le calcul des invariants, ce qui a conduit à une manipulation des prix, distordant ainsi le calcul du prix des BPT (Balancer Pool Token). Cette manipulation des invariants permet aux attaquants de tirer profit d'un échange en lot (batch swap) unique à partir d'un pool stable spécifique. Bien que certains chercheurs aient fourni des analyses perspicaces, certaines interprétations sont trompeuses, et la cause profonde ainsi que le processus d'attaque n'ont pas encore été complètement élucidés. Ce blog vise à fournir une analyse technique complète et précise de cet événement.

Points clés (TL;DR)

  • Cause fondamentale : incohérence d'arrondi et perte de précision
  • L'opération d'upscaling utilise l'arrondi unidirectionnel (arrondi vers le bas), tandis que l'opération de downscaling utilise l'arrondi bidirectionnel (arrondi vers le haut et vers le bas).
  • Cette incohérence entraîne une perte de précision et, lorsqu'elle est exploitée par des chemins d'échange soigneusement conçus, viole le principe standard selon lequel « l'arrondi doit toujours être en faveur du protocole ».
  • Exécution d'attaque
  • L'attaquant a soigneusement conçu les paramètres, y compris le nombre d'itérations et les valeurs d'entrée, pour maximiser l'impact de la perte de précision.
  • Les attaquants utilisent une méthode en deux étapes pour éviter la détection : d'abord, ils exécutent l'attaque principale dans une seule transaction sans réaliser immédiatement de profits, puis, dans une autre transaction, ils réalisent des bénéfices en retirant des actifs.
  • Impact opérationnel et amplification
  • En raison de certaines restrictions, le protocole ne peut pas être suspendu. Cette incapacité à arrêter les opérations a aggravé l'impact des attaques et a entraîné de nombreuses attaques ultérieures ou imitations.

Dans la section suivante, nous fournirons d'abord des informations clés sur Balancer V2, puis nous analyserons en profondeur les problèmes découverts et les attaques connexes.

0x1 Contexte

1. Pool de stabilité combiné Balancer V2

Le composant affecté par cette attaque est le pool de stabilité combiné du protocole Balancer V2. Ces pools sont spécialement conçus pour maintenir un ancrage proche de 1:1 (ou pour échanger à un taux connu) des actifs, et permettent d'effectuer des échanges de grande envergure avec un impact minimal sur le prix, augmentant ainsi considérablement l'efficacité du capital entre des actifs similaires ou connexes. Chaque pool dispose de son propre Token de pool Balancer (BPT), représentant la part des fournisseurs de liquidité dans le pool, ainsi que les actifs sous-jacents correspondants.

Ce pool utilise Stable Math (modèle StableSwap basé sur Curve), où la variable invariante D représente la valeur totale virtuelle du pool.

Le prix de BPT peut être approximé à :

ImageD peut être réduit sur le bilan (même sans aucune perte financière réelle), le prix du BPT apparaîtra alors moins cher.

2. batchSwap() et onSwap()

Balancer V2 offre la fonction batchSwap(), qui prend en charge les échanges multi-hop dans le Vault. Selon les paramètres passés à cette fonction, il existe deux types d'échanges :

  • GIVEN_IN (“Entrée donnée”) : L'appelant spécifie le nombre exact de tokens d'entrée, le pool calcule le nombre de tokens de sortie correspondant.
  • GIVEN_OUT (“Sortie donnée”) : L'appelant spécifie le nombre de sorties souhaitées, le pool calcule le nombre d'entrées nécessaires.

En général, batchSwap() est composé d'échanges entre plusieurs tokens effectués par la fonction onSwap(). Ce qui suit résume le chemin d'exécution lorsqu'une SwapRequest est attribuée comme type d'échange GIVEN_OUT (notez que ComposableStablePool hérite de BaseGeneralPool) :

! [image] ()

Voici le calcul de amount_in dans le type d'échange GIVEN_OUT, qui implique l'invariant D.

// inGivenOut jeton x pour y - équation polynomiale à résoudre // ax = montant à calculer // bx = solde de jetons dans // x = bx + ax (finalBalanceIn)
// D = invariant // A = coefficient d'amplification // n = nombre de jetons // S = somme des soldes finaux mais x // P = produit des soldes finaux mais x

               D                     D^(n+1)  

x^2 + ( S - ---------- - D) * x - ------------- = 0
(A * n^n) A * n^2n * P

3. Mise à l'échelle et arrondi

Pour normaliser le calcul entre différents soldes de Token, Balancer effectue les deux opérations suivantes :

  • Agrandissement (Upscaling) : Avant d'effectuer le calcul, agrandir le solde et le montant à une précision interne uniforme.

! [image] ()

  • Réduction (Downscaling) : Convertir le résultat à sa précision d'origine et appliquer un arrondi directionnel (par exemple, le montant d'entrée est généralement arrondi vers le haut pour s'assurer que le pool ne soit pas sous-facturé, tandis que le montant de sortie est souvent arrondi vers le bas).

! [image] ()

Il est évident que l'agrandissement et le rétrécissement sont théoriquement des opérations appariées — respectivement la multiplication et la division. Cependant, il existe des incohérences dans l'exécution de ces deux opérations. Plus précisément, l'opération de rétrécissement a deux variantes ou directions : divUp et divDown. En revanche, l'opération d'agrandissement n'a qu'une seule direction, à savoir mulDown.

La raison de cette incohérence n'est pas claire. Selon les commentaires dans la fonction _upscale(), les développeurs estiment que l'impact de l'arrondi unidirectionnel est minime.

// L'agrandissement (Upscale) peut ne pas toujours arrondir dans la même direction : par exemple, lors d'un échange,

// Le solde des Tokens d'entrée doit être arrondi à l'entier supérieur, tandis que le solde des Tokens de sortie doit être arrondi à l'entier inférieur. C'est le seul endroit où nous arrondissons tous les montants dans la même direction,

// En raison de l'impact de cet arrondi, il est prévu qu'il soit minimal.

// (et à moins que _scalingFactor() ne soit remplacé, il n'y a pas d'erreur d'arrondi).

0x2 Analyse des vulnérabilités

Le problème fondamental provient de l'opération d'arrondi vers le bas effectuée lors de l'opération de mise à l'échelle (upscaling) dans la fonction BaseGeneralPool._swapGivenOut(). En particulier, _swapGivenOut() arrondit à tort swapRequest.amount vers le bas via la fonction _upscale(). Cette valeur arrondie est ensuite utilisée comme amountOut lors du calcul de amountIn via _onSwapGivenOut(). Ce comportement est en contradiction avec la pratique standard selon laquelle “l'arrondi doit être appliqué d'une manière favorable au protocole.”

! [image] ()

Par conséquent, pour un pool donné (wstETH/rETH/cbETH), le montant calculé amountIn sous-estime la quantité d'entrée réellement nécessaire. Cela permet aux utilisateurs d'échanger une quantité moindre d'un actif sous-jacent (par exemple wstETH) contre un autre actif (par exemple cbETH), ce qui entraîne une réduction de l'invariant D en raison de la diminution de la liquidité efficace. Par conséquent, le prix correspondant du BPT (wstETH/rETH/cbETH) devient artificiellement déprimé (deflated), car $\text{BPT price} = \frac{D}{\text{totalSupply}}$.

Analyse d'attaque 0x3

L'attaquant a exécuté

Attaque en deux étapes, pouvant viser à minimiser le risque d'être détecté :

  • Dans la première phase, le noyau est utilisé pour exécuter des transactions uniques, sans profit immédiat.
  • Dans la deuxième phase, l'attaquant réalise un profit en extrayant des actifs dans une autre transaction.

La première étape peut être subdivisée en deux phases : le calcul des paramètres et l'échange en lot. Ci-dessous, nous utilisons un exemple de transaction (TX) d'attaque sur Arbitrum pour illustrer ces phases.

Phase de calcul des paramètres

À ce stade, l'attaquant combine le calcul hors chaîne et la simulation sur chaîne, en ajustant précisément les paramètres de chaque saut dans la prochaine étape (échange en masse) en fonction de l'état actuel du pool de stabilité combiné (y compris le facteur d'échelle, le coefficient d'amplification, le taux de BPT, les frais d'échange et d'autres paramètres). Fait intéressant, l'attaquant a également déployé un contrat auxiliaire pour aider à ces calculs, ce qui pourrait être destiné à réduire le risque de “front-running”.

Tout d'abord, l'attaquant collecte des informations de base sur le pool cible, y compris le facteur d'échelle de chaque Token, les paramètres d'amplification, le taux de BPT et le pourcentage de frais d'échange. Ensuite, ils calculent une valeur clé trickAmt, qui est le nombre de Tokens ciblés manipulés utilisé pour provoquer une perte de précision.

Appelons le facteur d'échelle du Token cible sF, calculé comme suit :

ImagePour déterminer les paramètres utilisés à l'étape 2 de la prochaine phase (échange de masse), l'attaquant utilise le calldata suivant pour effectuer un appel simulé ultérieur à la fonction 0x524c9e20 du contrat auxiliaire :

uint256[] balances; // Solde des tokens de la piscine (n'inclut pas BPT) uint256[] scalingFactors; // facteur de mise à l'échelle pour chaque Token de la piscine uint tokenIn; // Indice du Token d'entrée simulé pour ce saut uint tokenOut; // Index du Token de sortie simulé dans ce saut uint256 amountOut; // Quantité de Token de sortie souhaitée uint256 amp; // Paramètre d'augmentation du bloc uint256 fee; // Pourcentage des frais d'échange du Bloc

Les données retournées sont :

uint256[] balances; // Solde des tokens de la piscine après échange (n'inclut pas BPT)

Plus précisément, le solde initial et le nombre d'itérations sont calculés hors chaîne et passés en tant que paramètres au contrat de l'attaquant (rapports respectifs de 100,000,000,000 et 25). À chaque itération, trois échanges sont effectués :

  1. Échange 1 : Augmenter le nombre de Token cible à trickAmt + 1, en supposant que la direction de l'échange est 0 → 1.
  2. Échange 2 : Continuez à échanger targetToken avec trickAmt, cela déclenchera l'arrondi vers le bas dans l'appel de _upscale().
  3. Échange 3 : Effectuer une opération d'échange inversée (1 → 0), la quantité à échanger est déterminée en prenant les deux chiffres décimaux les plus significatifs du solde actuel du Token dans le pool, c'est-à-dire en arrondissant à la baisse au multiple le plus proche de $10^{d-2}$, où $d$ est le nombre de chiffres décimaux. Par exemple, 324,816 → 320,000.

Veuillez noter qu'en raison de l'utilisation de la méthode de Newton–Raphson dans le calcul de StableMath, cette étape peut parfois échouer. Pour atténuer cette situation, l'attaquant a mis en œuvre deux tentatives de réessai, chacune utilisant 9/10 de la valeur originale comme valeur de secours.

Le contrat auxiliaire de l'attaquant dérive de la bibliothèque StableMath de Balancer V2, ce qui peut être prouvé par son message d'erreur personnalisé de style “BAL”.

Phase d'échange en masse

Ensuite, l'opération batchSwap() peut être décomposée en trois étapes :

  1. Étape 1 : L'attaquant échange le BPT (wstETH/rETH/cbETH) contre des actifs sous-jacents, afin d'ajuster avec précision le solde d'un Token (cbETH) à la limite du seuil d'arrondi (montant = 9). Cela crée les conditions pour la perte de précision à l'étape suivante.
  2. Étape 2 : Ensuite, l'attaquant utilise un montant soigneusement conçu (= 8) pour échanger entre un autre actif sous-jacent (wstETH) et cbETH. En raison de l'arrondi à la baisse lors de la mise à l'échelle du nombre de tokens, le Δx calculé devient légèrement plus petit (de 8.918 à 8), ce qui conduit à une sous-estimation de Δy, réduisant ainsi l'invariant (D, provenant du modèle StableSwap de Curve). Étant donné que $\text{BPT price} = \frac{D}{\text{totalSupply}}$ , le prix du BPT est artificiellement abaissé.

! [image] () 3. Étape 3 : L'attaquant échange à nouveau les actifs sous-jacents contre des BPT, rétablissant l'équilibre tout en profitant de la baisse du prix des BPT.

0x4: Attaque et perte

Nous avons résumé ces attaques et leurs pertes correspondantes dans le tableau ci-dessous, pour un total de pertes dépassant 125 millions de dollars.

! [image] ()

0x5 Conclusion

Cet événement implique une série de transactions d'attaque contre le protocole Balancer V2 et ses projets forkés, entraînant des pertes financières importantes. Après l'attaque initiale, un grand nombre de transactions de suivi et d'imitation ont été observées sur plusieurs chaînes. Cet événement souligne plusieurs leçons clés sur la conception et la sécurité des protocoles DeFi :

  • Comportement de l'arrondi et perte de précision : L'arrondi unidirectionnel (arrondi vers le bas) utilisé dans l'opération d'agrandissement (upscaling) est différent de l'arrondi bidirectionnel (arrondi vers le haut et vers le bas) utilisé dans l'opération de réduction (downscaling). Afin de prévenir des vulnérabilités similaires, le protocole doit adopter une arithmétique de plus haute précision et mettre en œuvre des vérifications robustes. Il est impératif de respecter le principe standard selon lequel “l'arrondi doit toujours être en faveur du protocole”.
  • Évolution des exploits : Les attaquants ont exécuté un exploit complexe en deux étapes, visant à contourner la détection. Dans la première étape, les attaquants ont exécuté l'exploitation principale dans une seule transaction, sans réaliser de profit immédiat. Dans la deuxième étape, les attaquants ont réalisé un profit en extrayant des actifs dans une autre transaction. Cela souligne à nouveau la course aux armements continue entre les chercheurs en sécurité et les attaquants.
  • Conscience opérationnelle et réponse aux menaces : Cet événement souligne l'importance des alertes opportunes concernant l'état d'initialisation et d'exploitation, ainsi que des mécanismes proactifs de détection et de prévention des menaces, afin d'atténuer les pertes potentielles causées par des attaques persistantes ou simulées.

Tout en maintenant la continuité des opérations et des affaires, les acteurs du secteur peuvent utiliser BlockSec Phalcon comme dernière ligne de défense pour protéger leurs actifs. L'équipe d'experts de BlockSec est prête à effectuer une évaluation de sécurité complète pour votre projet.

Lien de l'article :

Source :

BPT11.25%
Voir l'original
Cette page peut inclure du contenu de tiers fourni à des fins d'information uniquement. Gate ne garantit ni l'exactitude ni la validité de ces contenus, n’endosse pas les opinions exprimées, et ne fournit aucun conseil financier ou professionnel à travers ces informations. Voir la section Avertissement pour plus de détails.
  • Récompense
  • Commentaire
  • Reposter
  • Partager
Commentaire
0/400
Aucun commentaire
  • Épingler
Trader les cryptos partout et à tout moment
qrCode
Scan pour télécharger Gate app
Communauté
Français (Afrique)
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Bahasa Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)