The attacker used a large, previously accumulated balance of a special auxiliary token to request refunds far exceeding the amount of a new deposit. Writing the resulting huge numerical values caused the refund event to exceed NEAR's log limit of 16,384 bytes. Because of this, the Intents deposit handler could not complete the operation. After the failure, HOT returned the original assets backing the deposit, including USDT, but the internal Intents balance credited by an earlier successful operation remained in place.
Addresses and their roles
| Network | Address or account | Role |
|---|---|---|
| BSC | 0x09fd1f5d9f185067a92493e43aa259ea4ab3ad37 | The attacker, who received around $3.865M |
| BSC | 0x233c5370CCfb3cD7409d9A3fb98ab94dE94Cb4Cd | HOT Bridge treasury |
| NEAR | 20e6bfe4685d99004fc2095d3a0fc6158cd810ce8f801196141e183ed6e19d65 | The attacker's account, from which the batch deposits and withdrawal requests were initiated; also the recipient of the returned assets backing the deposit |
| NEAR | dd1fe58ec316ada5b673b88c14fe2fa2b7ede82080440901dfbd64b994241757 | The attacker's contract, the recipient of the internal balance and the callback contract for notifications |
| NEAR | 873518f0f5cf120d7a8e2a416337bba16d7ae622e91f7da3862f07e89dc2ba9e | The attacker's contract that became the recipient of the internal balance in the subsequent October 1 operation sequences that could be confirmed. Short form: 8735… |
| NEAR | v2_1.omni.hot.tg | The HOT bridge contract and the registry of NEP-245 multi-token representations |
| NEAR | intents.near | The Intents verifier contract and the internal registry of user balances |
Background
HOT Bridge, also called HOT Omni Bridge, links assets from external blockchains to the NEAR network and is integrated with NEAR Intents for token swaps. The project's public SDK is @hot-labs/omni-sdk.
The system involves three levels of accounting: the real asset on the external blockchain, held in the HOT treasury → the HOT-issued representation of that asset on NEAR → the user's internal balance in intents.near.
A normal incoming deposit goes through the following stages:
- The user deposits a real asset on the source blockchain.
- HOT verifies the deposit and authorizes the corresponding asset representation on NEAR.
- HOT mints the corresponding tokens on NEAR. These tokens are transferred to Intents, where the corresponding amount is reflected in the user's internal balance.
The Intents deposit handling code also supports an optional recipient notification. The system first credits the funds to the account and then calls the recipient application. That application can request a refund of part of the deposited amount.
In a normal refund, Intents must first remove the corresponding amount from the internal balance and then return the assets backing that balance. The handler burns the unused credit, that is, cancels the corresponding part of the internal balance, and tells HOT which amounts remained unused. HOT then returns the corresponding tokens to the original sender.
During the incident, the link between the second and third levels of accounting broke: HOT returned the deposit backing as representation tokens on NEAR, but Intents kept the user's internal balance. This later made it possible to request withdrawals from the treasury through the bridge's normal withdrawal mechanism.
1. Preparation
On September 29, the attacker received 10 BSC USDT on NEAR in transaction 9SiSBv….
The attacker also transferred from the Gonka network 97 × 10³⁶ + 97 minimal units of the special auxiliary asset ctfhote6ae2a90d1836ef1. The original deposit matches the later receipt of the asset on NEAR in transaction Fmwao…. As a result, HOT representation tokens were minted to address 20e6….
Then, in transaction 4wf2…, the attacker deposited 97 × 10³⁶ units of the auxiliary asset into Intents for the attack's recipient contract dd1…. 97 minimal units remained outside Intents. This created the large internal balance needed for the later oversized refund requests.
2. Exploiting the vulnerability
The first confirmed exploitation operation, 3k91…, began on September 29 at 16:14:19.280 UTC.
The attacker called the HOT function mt_batch_transfer_call, depositing into Intents 97 entries of the auxiliary token of one unit each, along with 10 USDT. Intents successfully credited the funds to the internal balance of dd1… before notifying the recipient contract controlled by the attacker. The recipient contract requested refunds of 97 amounts of the auxiliary token, 10³⁶ units each, and then indicated that no USDT refund was needed.
The vulnerable refund handler capped the refund amount at the available balance, but did not take the size of the current deposit into account. As a result, the pre-prepared balance made it possible to include huge refund amounts in the token burn event.
Here, the variable balance_left means the recipient's entire internal balance in a given asset, including previous deposits. The check determined whether the recipient had enough funds on the balance, but did not cap the refund at the amount deposited in the current operation.
As a result, a deposit entry of just one unit could produce a refund request for 10³⁶ units if the recipient's previously accumulated balance could cover that amount. Each calculated non-zero refund amount was then added to the token burn event, on lines 198–199 of the code above.
The size of the resulting event was 16,495 bytes:
| Component | Calculation | Bytes |
|---|---|---|
| 97 full token IDs, 125 characters each | 97 × (125 characters + 2 quotes) + 96 commas | 12,415 |
| 97 refund amounts, each a quoted 37-digit string | 97 × (37 digits + 2 quotes) + 96 commas | 3,879 |
| EVENT_JSON prefix, metadata, owner ID, memo and other JSON characters | Fixed size | 201 |
| Total | 12,415 + 3,879 + 201 | 16,495 |
The error in forming the event rolled back the earlier balance changes made by the refund handler. HOT then returned the entire original deposit batch, including the 10 USDT. The deposit handler had called self.deposit(...) before scheduling the recipient notification and the subsequent call to mt_resolve_deposit.
So a failure of a later call does not automatically undo changes that were successfully made within an earlier call. This behavior is described in the NEAR documentation. The internal Intents balance credited during the earlier successful stage remained in place.
3. Scaling up
In one of the later operations, 3mst2uZ32KPuCs7wc8rJqEtCrZqugCy4yDSZ3df8KPm3, the attacker deposited 649,984 USDT and requested a refund of exactly the same amount to the sender.
The reconstructed transaction history contains 23 batch calls linked to refund handler failures. The full pattern, in which the internal balance was retained while the deposit backing was returned, has been reliably confirmed for six cases.
Synchronization problem: how the Verus protocol lost $7.5M due to differences between blockchainsAug 3, 2026Read more4. Withdrawing the funds
The attacker then sent withdrawal requests from NEAR through HOT, specifying the BSC address 0x09fd…ad37 as the recipient. Five requests correspond to successful USDT payouts from the HOT treasury on BSC at 0x233c…4Cd.
| BSC payout time, UTC | USDT | NEAR request | BSC payout |
|---|---|---|---|
| September 30, 23:54:30 | 800,000 | A1tNAg5w3NVrDGheG9zTNmrXeWd4249nYyVLF8ekK3Xh | 0x9fe58e031f73bbd880c782bc9e7446bcda32cadb304a729209871a4a81856c4c |
| October 1, 00:24:02 | 1,200,000 | 799TeaBfrtCcmJ5dMT6fqupqLHyZQAqm2ksKoEfyMj7J | 0x0381265d6a1bb09de899f49f410a8b907cd548358a7e3c91076c3a52656b8220 |
| October 1, 00:50:42 | 1,500,000 | GJ65AKDXN9ov7DTFgmgvbykNww1B5J7jK42A4QfbHgon | 0x69d1c68c7e961a0199d3c9f3b6a31cb168ed775ef34b51a42762253ac1efcceb |
| October 1, 01:46:20 | 330,000 | E4gEvzNSPjeiah5c6pvUWTXkUmiiwjkbWr2zzQWsz6it | 0x9c10b967da0c85631ec105e3b322c0a851fcd01b69ff390ff58f860e5cce003a |
| October 1, 06:08:26 | 35,000 | Avf5qSFyndojAP7aYYMw2sCUZj2JSHUxYhK8CVGisy8B | 0x16ddfa695b9525662b8e57c35723f44af89b4e7fc2dedc3634f730caf8b90c02 |
| Total | 3,865,000 |
The vulnerability
1. The internal balance is credited before the recipient responds to the refund request
The Intents implementation for handling incoming deposits first calls:
self.deposit( receiver_id.clone(), core_token_ids .clone() .zip(amounts.iter().map(|amount| amount.0)), Some("deposit"), ) .unwrap_or_else(|err| err.panic());
For a deposit with notification, the system then schedules a call to the recipient contract and a subsequent call to mt_resolve_deposit. The stage that creates the internal balance can complete successfully before the subsequent refund handler throws an error. The final execution result tells HOT that the later operation failed, even though the internal balance created earlier already exists.
2. The vulnerable calculation allows a refund larger than the current deposit
NEAR's October 1 commit 35044fd…, titled «chore: TEMP change that reflects deployed state», reproduces the refund logic as follows:
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.
This calculation takes the available balance into account, but does not cap the refund at the amount of the current deposit. The loop adds each non-zero refund amount to the burn event while subtracting it from the recipient's balance and from the total internal token supply. After that, the following call is made:
MtEvent::MtBurn([burn_event].as_slice().into()).emit();
These accounting changes are made within the refund handler's receipt. If writing the event fails, all changes made in that receipt are rolled back along with it.
3. A possible discrepancy between the published code and the deployed version
The missing cap of the refund amount at the size of the current deposit was fixed in the public repository on March 23, 2026, at 12:47:07 UTC. The fix was published in PR #240 / commit 015fa811…, titled «fix: cap notify refunds by the original amounts». In the fixed version, the calculation looks like this:
let refund_amount = requested_refund.min(deposited).min(balance_left);*
For the one-unit auxiliary entries used in the attack, this caps each refund at one unit at most, regardless of the previously accumulated balance. This mechanism prevents refund amounts from being multiplied with oversized numbers. It also restores the core rule of deposit accounting: a refund for the current operation cannot take funds deposited in previous deposits.
The temporary October 1 commit removes this cap in order to reproduce the behavior of the deployed version. The note to NEAR's October regression test states explicitly that oversized refund events caused the callback to fail «as it happened in the deployed version».
Response and recovery
After the incident, an upgrade was carried out, reflected in transaction FPLaCtd…. The main transaction, executed on October 2 at 20:31:41.711 UTC, calls ctl-intents.near.upgrade for the intents.near contract. The controller event lists release version 0.4.4.
A successful nested deployment receipt, recorded at 20:31:45.442 UTC, deployed the code and executed the state_migrate function. Alex Shevchenko publicly stated that the full $3.86M had been returned.

