À seulement 111 octets de l'échec: comment une vulnérabilité de HOT Omni Bridge a entraîné une perte de $3.86M

À seulement 111 octets de l'échec: comment une vulnérabilité de HOT Omni Bridge a entraîné une perte de $3.86M

Les faits suggèrent un possible écart entre le code source public corrigé et la version du contrat réellement exécutée sur le réseau.

12 oct. 2026

L'attaquant a utilisé un solde important, accumulé au préalable, d'un token auxiliaire spécial pour demander des remboursements dépassant largement le montant d'un nouveau dépôt. L'écriture des valeurs numériques énormes qui en résultaient a fait dépasser à l'événement de remboursement la limite de journalisation de NEAR de 16,384 octets. De ce fait, le gestionnaire de dépôt d'Intents n'a pas pu terminer l'opération. Après cet échec, HOT a renvoyé les actifs originaux garantissant le dépôt, y compris l'USDT, mais le solde interne d'Intents crédité par une opération réussie antérieure est resté en place.

Adresses et leurs rôles

RéseauAdresse ou compteRôle
BSC0x09fd1f5d9f185067a92493e43aa259ea4ab3ad37L'attaquant, qui a reçu environ $3.865M
BSC0x233c5370CCfb3cD7409d9A3fb98ab94dE94Cb4CdTrésorerie de HOT Bridge
NEAR20e6bfe4685d99004fc2095d3a0fc6158cd810ce8f801196141e183ed6e19d65Le compte de l'attaquant, à partir duquel les dépôts groupés et les demandes de retrait ont été initiés; également le destinataire des actifs renvoyés garantissant le dépôt
NEARdd1fe58ec316ada5b673b88c14fe2fa2b7ede82080440901dfbd64b994241757Le contrat de l'attaquant, destinataire du solde interne et contrat de rappel pour les notifications
NEAR873518f0f5cf120d7a8e2a416337bba16d7ae622e91f7da3862f07e89dc2ba9eLe contrat de l'attaquant qui est devenu le destinataire du solde interne lors des séquences d'opérations ultérieures d'octobre 1 qui ont pu être confirmées. Forme abrégée: 8735…
NEARv2_1.omni.hot.tgLe contrat du pont HOT et le registre des représentations multi-tokens NEP-245
NEARintents.nearLe contrat vérificateur d'Intents et le registre interne des soldes utilisateurs

Contexte

HOT Bridge, également appelé HOT Omni Bridge, relie les actifs provenant de blockchains externes au réseau NEAR et est intégré à NEAR Intents pour les échanges de tokens. Le SDK public du projet est @hot-labs/omni-sdk.

Le système comporte trois niveaux de comptabilisation: l'actif réel sur la blockchain externe, détenu dans la trésorerie de HOT → la représentation de cet actif émise par HOT sur NEAR → le solde interne de l'utilisateur dans intents.near.

Un dépôt entrant normal passe par les étapes suivantes:

  • L'utilisateur dépose un actif réel sur la blockchain source.
  • HOT vérifie le dépôt et autorise la représentation correspondante de l'actif sur NEAR.
  • HOT émet les tokens correspondants sur NEAR. Ces tokens sont transférés vers Intents, où le montant correspondant est reflété dans le solde interne de l'utilisateur.

Le code de traitement des dépôts d'Intents prend également en charge une notification facultative du destinataire. Le système crédite d'abord les fonds sur le compte, puis appelle l'application du destinataire. Cette application peut demander le remboursement d'une partie du montant déposé.

Dans un remboursement normal, Intents doit d'abord retirer le montant correspondant du solde interne, puis renvoyer les actifs garantissant ce solde. Le gestionnaire brûle le crédit inutilisé, c'est-à-dire annule la partie correspondante du solde interne, et indique à HOT quels montants sont restés inutilisés. HOT renvoie alors les tokens correspondants à l'expéditeur d'origine.

Lors de l'incident, le lien entre les deuxième et troisième niveaux de comptabilisation s'est rompu: HOT a renvoyé la garantie du dépôt sous forme de tokens de représentation sur NEAR, mais Intents a conservé le solde interne de l'utilisateur. Cela a ensuite permis de demander des retraits depuis la trésorerie via le mécanisme de retrait normal du pont.

1. Préparation

Le 29 septembre, l'attaquant a reçu 10 USDT BSC sur NEAR dans la transaction 9SiSBv….

L'attaquant a également transféré depuis le réseau Gonka 97 × 10³⁶ + 97 unités minimales de l'actif auxiliaire spécial ctfhote6ae2a90d1836ef1. Le dépôt initial correspond à la réception ultérieure de l'actif sur NEAR dans la transaction Fmwao…. En conséquence, des tokens de représentation HOT ont été émis à l'adresse 20e6….

Puis, dans la transaction 4wf2…, l'attaquant a déposé 97 × 10³⁶ unités de l'actif auxiliaire dans Intents pour le contrat destinataire de l'attaque dd1…. 97 unités minimales sont restées en dehors d'Intents. Cela a créé le solde interne important nécessaire aux demandes de remboursement surdimensionnées ultérieures.

2. Exploitation de la vulnérabilité

La première opération d'exploitation confirmée, 3k91…, a débuté le 29 septembre à 16:14:19.280 UTC.

L'attaquant a appelé la fonction HOT mt_batch_transfer_call, en déposant dans Intents 97 entrées du token auxiliaire d'une unité chacune, accompagnées de 10 USDT. Intents a crédité avec succès les fonds sur le solde interne de dd1… avant de notifier le contrat destinataire contrôlé par l'attaquant. Le contrat destinataire a demandé des remboursements de 97 montants du token auxiliaire, 10³⁶ unités chacun, puis a indiqué qu'aucun remboursement en USDT n'était nécessaire.

Le gestionnaire de remboursement vulnérable plafonnait le montant du remboursement au solde disponible, mais ne tenait pas compte de la taille du dépôt en cours. Par conséquent, le solde préparé à l'avance a permis d'inclure des montants de remboursement énormes dans l'événement de destruction de token.

Ici, la variable balance_left désigne l'ensemble du solde interne du destinataire dans un actif donné, y compris les dépôts précédents. Le contrôle vérifiait si le destinataire disposait de fonds suffisants sur son solde, mais ne plafonnait pas le remboursement au montant déposé lors de l'opération en cours.

Par conséquent, une entrée de dépôt d'une seule unité pouvait produire une demande de remboursement de 10³⁶ unités si le solde précédemment accumulé du destinataire pouvait couvrir ce montant. Chaque montant de remboursement calculé non nul était ensuite ajouté à l'événement de destruction de token, aux lignes 198-199 du code ci-dessus.

La taille de l'événement résultant était de 16,495 octets:

ComposantCalculOctets
97 identifiants complets de token, 125 caractères chacun97 × (125 caractères + 2 guillemets) + 96 virgules12,415
97 montants de remboursement, chacun une chaîne entre guillemets de 37 chiffres97 × (37 chiffres + 2 guillemets) + 96 virgules3,879
Préfixe EVENT_JSON, métadonnées, ID du propriétaire, mémo et autres caractères JSONTaille fixe201
Total12,415 + 3,879 + 20116,495

L'erreur dans la formation de l'événement a annulé les modifications de solde précédemment effectuées par le gestionnaire de remboursement. HOT a ensuite renvoyé l'intégralité du lot de dépôt d'origine, y compris les 10 USDT. Le gestionnaire de dépôt avait appelé self.deposit(...) avant de programmer la notification du destinataire et l'appel ultérieur à mt_resolve_deposit.

Ainsi, l'échec d'un appel ultérieur n'annule pas automatiquement les modifications effectuées avec succès lors d'un appel antérieur. Ce comportement est décrit dans la documentation de NEAR. Le solde interne Intents crédité lors de l'étape précédente réussie est resté en place.

3. Mise à l'échelle

Dans l'une des opérations ultérieures, 3mst2uZ32KPuCs7wc8rJqEtCrZqugCy4yDSZ3df8KPm3, l'attaquant a déposé 649,984 USDT et demandé un remboursement du montant exactement identique à l'expéditeur.

L'historique des transactions reconstitué contient 23 appels par lots liés à des échecs du gestionnaire de remboursement. Le schéma complet, dans lequel le solde interne était conservé pendant que la garantie du dépôt était restituée, a été confirmé de manière fiable pour six cas.

Problème de synchronisation: comment le protocole Verus a perdu $7.5M à cause des différences entre les blockchains3 août 2026En savoir plus

4. Retrait des fonds

L'attaquant a ensuite envoyé des demandes de retrait depuis NEAR via HOT, en indiquant l'adresse BSC 0x09fd…ad37 comme destinataire. Cinq demandes correspondent à des versements d'USDT réussis depuis la trésorerie HOT sur BSC à 0x233c…4Cd.

La vulnérabilité

1. Le solde interne est crédité avant que le destinataire ne réponde à la demande de remboursement

L'implémentation Intents pour le traitement des dépôts entrants appelle d'abord:

self.deposit( receiver_id.clone(), core_token_ids .clone() .zip(amounts.iter().map(|amount| amount.0)), Some(«deposit»), ) .unwrap_or_else(|err| err.panic());

Pour un dépôt avec notification, le système planifie ensuite un appel au contrat du destinataire, puis un appel ultérieur à mt_resolve_deposit. L'étape qui crée le solde interne peut se terminer avec succès avant que le gestionnaire de remboursement ultérieur ne génère une erreur. Le résultat final de l'exécution indique à HOT que l'opération ultérieure a échoué, alors même que le solde interne créé précédemment existe déjà.

2. Le calcul vulnérable permet un remboursement supérieur au dépôt actuel

Le commit du 1 octobre de NEAR, 35044fd…, intitulé «chore: TEMP change that reflects deployed state», reproduit la logique de remboursement comme suit:

let requested_refund = requested_refund.unwrap_or(deposited); let balance_left = receiver.token_balances.amount_for(&token_id); let refund_amount = balance_left.min(requested_refund);*

Source: resolve_deposit_internal.

Ce calcul prend en compte le solde disponible, mais ne plafonne pas le remboursement au montant du dépôt actuel. La boucle ajoute chaque montant de remboursement non nul à l'événement de burn tout en le soustrayant du solde du destinataire et de l'offre totale interne du token. Ensuite, l'appel suivant est effectué:

MtEvent::MtBurn([burn_event].as_slice().into()).emit();

Ces modifications comptables sont effectuées dans le reçu du gestionnaire de remboursement. Si l'écriture de l'événement échoue, toutes les modifications effectuées dans ce reçu sont annulées avec lui.

3. Une possible divergence entre le code publié et la version déployée

L'absence de plafonnement du montant du remboursement à la taille du dépôt actuel a été corrigée dans le dépôt public le 23 mars 2026, à 12:47:07 UTC. Le correctif a été publié dans la PR #240 / commit 015fa811…, intitulée «fix: cap notify refunds by the original amounts». Dans la version corrigée, le calcul se présente comme suit:

let refund_amount = requested_refund.min(deposited).min(balance_left);*

Pour les entrées auxiliaires d'une unité utilisées dans l'attaque, cela plafonne chaque remboursement à une unité au maximum, indépendamment du solde précédemment accumulé. Ce mécanisme empêche la multiplication des montants de remboursement par des nombres surdimensionnés. Il rétablit également la règle fondamentale de la comptabilité des dépôts: un remboursement pour l'opération en cours ne peut pas prélever des fonds déposés lors de dépôts précédents.

Le commit temporaire du 1 octobre supprime ce plafond afin de reproduire le comportement de la version déployée. La note relative au test de régression d'octobre de NEAR indique explicitement que des événements de remboursement surdimensionnés ont provoqué l'échec du callback «comme cela s'est produit dans la version déployée».

Réponse et récupération

Après l'incident, une mise à niveau a été réalisée, reflétée dans la transaction FPLaCtd…. La transaction principale, exécutée le 2 octobre à 20:31:41.711 UTC, appelle ctl-intents.near.upgrade pour le contrat intents.near. L'événement du contrôleur indique la version de publication 0.4.4.

Un reçu de déploiement imbriqué réussi, enregistré à 20:31:45.442 UTC, a déployé le code et exécuté la fonction state_migrate. Alex Shevchenko a publiquement déclaré que la totalité des $3.86M avait été restituée.

#Tether#Piratage#DeFi
L'USDT a été renvoyé, mais le solde n'a jamais été débité: comment HOT Omni Bridge a été attaqué