Just 111 bytes from failure: how a HOT Omni Bridge vulnerability led to a $3.86M loss

Just 111 bytes from failure: how a HOT Omni Bridge vulnerability led to a $3.86M loss

The facts point to a possible discrepancy between the fixed public source code and the contract version actually running on the network.

Oct 12, 2026

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

NetworkAddress or accountRole
BSC0x09fd1f5d9f185067a92493e43aa259ea4ab3ad37The attacker, who received around $3.865M
BSC0x233c5370CCfb3cD7409d9A3fb98ab94dE94Cb4CdHOT Bridge treasury
NEAR20e6bfe4685d99004fc2095d3a0fc6158cd810ce8f801196141e183ed6e19d65The attacker's account, from which the batch deposits and withdrawal requests were initiated; also the recipient of the returned assets backing the deposit
NEARdd1fe58ec316ada5b673b88c14fe2fa2b7ede82080440901dfbd64b994241757The attacker's contract, the recipient of the internal balance and the callback contract for notifications
NEAR873518f0f5cf120d7a8e2a416337bba16d7ae622e91f7da3862f07e89dc2ba9eThe 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…
NEARv2_1.omni.hot.tgThe HOT bridge contract and the registry of NEP-245 multi-token representations
NEARintents.nearThe 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:

ComponentCalculationBytes
97 full token IDs, 125 characters each97 × (125 characters + 2 quotes) + 96 commas12,415
97 refund amounts, each a quoted 37-digit string97 × (37 digits + 2 quotes) + 96 commas3,879
EVENT_JSON prefix, metadata, owner ID, memo and other JSON charactersFixed size201
Total12,415 + 3,879 + 20116,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 more

4. 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.

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.

#Tether#Hack#DeFi
USDT returned, but the balance was never debited: how HOT Omni Bridge was attacked