rekt — polkadot exploit post-mortems

dotUSD — Rekt

A four-month-old fix, a silent patch, and a stablecoin that forgot how to count.
27 October 2026 · ~$1.4M · Polkadot Asset Hub
LIVE — connecting to a Polkadot archive node via polkadot-api…
Live state · Polkadot Asset HubValueWhat it means
Recorded dotUSD supply—what the chain thinks exists
Actual backing · USDT reserve—what actually exists
Phantom gap—still mispriceable, right now
bkchr balance, unredeemed—≈3.95M of it rescued from the pool
Pool left · dUSD / DOT—the drained husk

Another day, another Polkadot stablecoin that forgot how to count.

On 24 October 2026 the dotUSD liquidity pool on Polkadot Asset Hub began selling DOT far below its market price. Over the next 38 hours the pool went from 2.63 million DOT and 2.73 million dotUSD to about 45,000 of each. The pool belongs to the Polkadot Treasury, which is its only liquidity provider, so the money that left it was Treasury money.

The cause was not a reentrancy bug or a flash loan. It was an accounting error in Polkadot’s XCM executor that let anyone reduce dotUSD’s recorded total supply without changing any account balance. The pool’s pricing depends on that supply figure, so once it was wrong the pool mispriced its own reserves and sold DOT for almost nothing.

The net loss to the Treasury was about $1.4 million. It would have been closer to $5.4 million if a member of the Polkadot Fellowship had not intervened on-chain to move the remaining 4 million dotUSD out of the pool before anyone else could.

The fix for the underlying bug had been public for four months. It was merged to the main development branch in June and was not shipped to the release branch Asset Hub runs until 22 October, three days before the attack began.

Background: dotUSD

dotUSD is Polkadot’s native stablecoin. Users mint it by depositing USDT into a Peg Stability Module one for one, and redeem it the same way. The reserve behind it holds 4.21 million USDT and was never touched in any part of this incident. The referendum that created it passed with 98.4 percent approval. On 8 October governance created the asset on Asset Hub and seeded a DOT/dotUSD pool on Asset Hub’s built-in AMM with 2.70 million DOT and 2.64 million dotUSD.

Two facts about the setup matter later. The first is that the Treasury owns essentially all of the pool’s liquidity, so any loss falls entirely on the Treasury. The second is that the asset’s owner, admin, issuer and freezer roles all belong to a single account, 12Sg2E9v…. That account is not the Treasury, not a governance body, and not a multisig. It holds no DOT, has no identity, and has never signed a transaction, so it cannot even pay a fee. It is the only account that can freeze or mint dotUSD.

The bug

Every asset on Asset Hub tracks two things: the balance of each account, and a separate total supply. The two should always agree.

The XCM executor, which handles cross-chain messages, carried a regression introduced in February. When a message withdrew two assets and the second withdrawal failed, the executor rolled the transaction back and correctly restored the withdrawn balance. But the value that tracked the withdrawal was constructed outside the scope of that rollback. When it was dropped at the end of execution it decremented the total supply, and that decrement was not rolled back.

So the balance is restored while the recorded supply falls. A message carrying 1,000 real dotUSD and one unit of an asset guaranteed to fail erases 1,000 dotUSD from the supply figure at no cost, and it can be repeated.

This would be harmless if nothing treated the supply figure as authoritative. The AMM does. When the pool reads its dotUSD reserve, pallet-assets returns the smaller of the real balance and the total supply. Once the recorded supply fell below the pool’s actual holdings, the pool believed it held almost no dotUSD, priced it accordingly, and handed out DOT for small amounts of dotUSD.

The fix that did not ship

The bug was introduced on 16 February in polkadot-sdk #10384, a refactor of the XCM executor’s imbalance handling by the Parity engineer Adrian Catangiu. The bug itself is not the interesting part. It was subtle, it sat on the path that moves every bridged asset, and it passed review. The missing test for the relevant invariant, that a failed withdrawal must not change supply, is an ordinary oversight. What matters is what happened after it was found.

It was fixed in #11910, merged to master on 25 June. The pull request is titled “XCM hardening - fix some regressions,” and its description discusses deposit retries, dust, and BLS keys. It does not mention WithdrawAsset, supply, or burned funds, and there is no security advisory or CVE. The fix is a one-line change that moves a variable inside a transaction block, visible only to someone reading the diff.

When #11910 merged it was backported to the older stable2603 line on 26 June. The stable2606 line, which is the line Asset Hub’s runtime is built from, had been released on 3 July without the fix. It was not backported to stable2606 until #13531 on 22 October. For about four months the production release line ran without a fix that had been in master since June, and nothing in the release process caught it.

That is a Parity release-engineering failure. A fix that stopped funds from being burned reached an older maintenance branch and not the production branch, and no one noticed for a third of a year.

The Fellowship’s position is more involved. It builds and enacts the Asset Hub runtime from Parity’s release line, so it could not enact a fix that was never placed there. What it did not do is run any process that would have caught the gap: no tracking of security-relevant fixes against the runtime it ships, no monitoring of the supply invariant on a Treasury-backed stablecoin, and, once the exploit was public, no action for 39 hours on an emergency track that works in hours. For a Treasury-funded body whose mandate is runtime stewardship, that absence of basic release engineering and operations is the governance failure here.

The surrounding context is worth stating plainly, since others will raise it. During the same period the Fellowship was short of money. Its salary account was running low, and a 4.8 million DOT top-up, Referendum 1940, was rejected by roughly 99 percent and cancelled by Referendum 1941, which passed with 99.2 percent. By the Fellowship’s own projection its salary runway ran out at the end of September. None of the money from this exploit went to anyone connected to the Fellowship. Every account that profited is an ordinary trading bot with no on-chain link to an insider, and we checked. But a technical steward that was underfunded and distracted, and that left a known funds-burning bug in production for four months, is going to be asked where its attention was.

Timeline

All times UTC.

24 October, 13:04. An account buys 1,000 dotUSD from the pool for 969 DOT. That is the only dotUSD the attack needs.

14:43. A single bridge message erases 1 dotUSD of supply, confirming the bug still works.

16:05. Over the next hour, five bursts of Snowbridge messages from Ethereum, 203 messages in all, each running 20 nested failing withdrawals, remove 2.48 million dotUSD from the recorded supply. In the block after each burst, arbitrage bots buy the mispriced DOT. By 17:35 the pool holds 1.28 million DOT, down from 2.63 million.

20:33. Hydration reports that the same bug is destroying its users’ bridge transfers in transit, pauses Snowbridge in its own interface, and describes the funds as unrecoverable without governance action. About $3,700 of ordinary users’ USDC and ETH had already been burned. Nothing was paused on-chain.

26 October, 01:07 to 05:04. Four more bursts erase another 1.58 million dotUSD. The pool is left with 46,000 DOT and 4.0 million dotUSD it no longer believes it holds.

11:54. The fixed runtime is enacted, 39 hours after Hydration’s public report. New burns stop. The supply figure is still wrong, so the pool still holds about $4 million that remains extractable by anyone using ordinary mint, redeem and swap calls.

No monitoring

The imbalance was simple to detect. A token’s recorded supply should equal the sum of its balances, which is the most basic solvency check available. That equation broke on the first test message, 80 minutes before the main attack, and stayed broken, drifting by millions, for a day and a half. No system on the Polkadot side noticed. The first warning came from Hydration, watching its own users’ funds disappear. There was no monitor on the one figure that determines whether the stablecoin is solvent.

The rescue

At 15:43 on 26 October, four hours after the fix was enacted, an account with the on-chain identity bkchr, a senior Fellowship member, emptied the remaining pool in a single transaction, moving 3.95 million dotUSD into its own account, where it still sits.

This looks like the final theft and is the opposite. The pool was still mispriced and still held about $4 million that the next bot to run the arithmetic would have taken. The fix had stopped new supply from being erased but had not restored the supply already lost. The only way to secure the remaining funds was to use the same mispricing deliberately: mint dotUSD to raise the supply figure, swap DOT out at the correct price, redeem to lower the figure again, then swap a small amount of dotUSD back to recover the DOT. The transaction ran this loop 55 times. It cost the operator working capital and left the Treasury down $1.4 million instead of $5.4 million. The 3.95 million dotUSD is held pending return to the Treasury.

It raises a fair question. If a Fellowship member could see the open $4 million and move on it within hours of the fix, why did the attacker, who had the pool at the floor and ten hours of daylight, not take it first?

Where the money went

The value that left the pool, about $1.4 million, was taken by arbitrage bots buying mispriced DOT, not by the attacker.

Each is a professional trading bot funded from exchange hot wallets, one of them a Binance wallet, and each sent its profits back to those exchanges. Those balances sit behind exchange KYC. The Treasury has a recovery route there, since these were windfalls from a broken oracle and the exchanges can be asked to identify the accounts.

The attacker took almost nothing. Their known wallets spent about 1,100 DOT and a few dollars of ETH on setup and ended with a 1,000 dotUSD position they never sold. The funding is deliberately hard to trace. The operating budget came from Monero, swapped through a privacy service into fresh Hyperliquid wallets, bridged across two chains into a throwaway deployer that created the attack contract and three burner accounts, which sent the bridge messages and swept their remaining balances. The dotUSD used as ammunition came from what looks like another swap-service wallet. Every role used a fresh account, and the trail ends at a Monero swap service.

Attribution

The natural question is who did this. The honest answer is that the chain does not identify anyone, and it is worth being precise about what the evidence does and does not support.

Some facts invite suspicion. The attacker moved within three days of the fix reaching Asset Hub’s branch, not when it went public in June and not at an arbitrary later date, but in the short window after the backport appeared and before the runtime was enacted. They knew the fix was silent, which you learn by reading the executor internals rather than the pull request text. They understood the full chain: the rollback flaw, the supply cap in pallet-assets, the AMM’s pricing, Snowbridge’s permissionless message appending, and the mapping from an Ethereum address to an Asset Hub account. This is not opportunistic work.

Other facts cut against any firm conclusion. Every account that profited is an ordinary, long-lived arbitrage bot with no on-chain connection to the attacker’s Monero-funded wallets. We checked the strongest version of the theory, whether any account both erased supply and swapped in the same block, which only a coordinated actor could do, and it did not happen. The burns occur in the bridge’s message-processing phase and the arbitrage is reactive, one block later, which is how an independent bot behaves. The attacker took nothing and used Monero-grade operational security. That is consistent with a griefer, and it is equally consistent with someone who meant to profit through a separate, exchange-funded bot kept deliberately unlinked.

That last possibility is the one the chain cannot resolve. An attacker who wanted to drain the Treasury and stay clean would do roughly what happened here: break the pool with throwaway wallets and let an ordinary-looking bot take the spread a block later. On-chain that bot is indistinguishable from a real one. Only the exchanges holding the KYC behind the profiting accounts can say whether any of them belongs to someone who should not have it.

We are not naming anyone, because the evidence does not. What the evidence shows is a public fix left unshipped on the production branch for four months and a disclosure that treated a funds-burning bug as a routine cleanup commit. Whether the person who used it was an outsider who reads diffs or someone who already knew is a question for the Fellowship, the Web3 Foundation, and Parity to answer, and for the exchanges and swap services that hold the relevant records.

The Fellowship’s statement

On 27 October bkchr posted the Fellowship’s statement to the Polkadot forum. The numbers in it are accurate and match an independent trace closely: about 4.06 million dotUSD wrongly removed from supply, the tokens left in the pool, arbitrage traders targeting the imbalance, and an estimated $1.3 million reduction to the Treasury’s liquidity position. It confirms the funds were moved to a controlled account to prevent further extraction, which is the rescue, and it promises a corrective referendum and a full post-mortem.

Two claims in the statement do not hold up.

The first is that the incident “did not affect user wallets or funds” and that “no action is required.” That is true only for the dotUSD pool. The same bug burned ordinary users’ bridge transfers in transit. Hydration reported Hydration-to-Ethereum transfers that succeeded on Hydration while the underlying assets were destroyed on Asset Hub, unrecoverable without governance action, and paused Snowbridge in response on 24 October.

The second is that the Fellowship “discovered an XCM issue over the weekend.” The fix was merged publicly in June, flagged publicly by Hydration on 24 October, and left unshipped on Asset Hub’s branch until the 26th. The statement also notes that the fix was enacted through a whitelisted-caller referendum, the Fellowship’s emergency fast-track, which means the capacity to act in hours existed throughout and was used only after the pool had been drained.

Questions the post-mortem should answer

The Fellowship has promised a full post-mortem. These are the questions it needs to answer.

  1. Release process. Who owns ensuring a security fix reaches every live release branch, not only the branch it was first backported to? Why did stable2606 ship on 3 July without the fix, and why was the backport queued only on 21 October, three days before the attack?
  2. Monitoring. The supply-versus-balances invariant broke on the first test message and drifted for a day and a half with no alarm. Will supply monitoring be added for dotUSD and every other sufficient asset, and who operates it?
  3. Incident response. The fix was enacted through the whitelisted-caller fast-track, which acts in hours. Hydration flagged the bug publicly at 20:33 on 24 October and the fix enacted 39 hours later. Why was nothing paused on-chain in that window, whether Snowbridge inbound, the PSM, or the pool?
  4. Launch. dotUSD went live on 8 October onto a runtime whose fix had been public for three months. Who signed off on launching a Treasury-backed stablecoin onto that runtime, and was its security state assessed at all?
  5. User funds. The statement says no user funds were affected. Hydration says otherwise. What is the total value destroyed across all assets for the full period the vulnerable executor was live, roughly February to 26 October?
  6. The admin key. Why does unilateral freeze and mint authority over a Treasury-backed stablecoin sit on a single unfunded keypair with no identity and no multisig?
  7. Recovery. Will the Treasury pursue recovery from the profiting accounts through the exchanges that hold their identities?
  8. The correction. What is the exact mechanism and timeline for repairing the supply figure, and what is the custody and return path for the 3.95 million dotUSD in the rescue account?

What is left

The bug is fixed. Two problems remain.

The recorded supply is still wrong. As of writing it reads about 153,000 against 4.21 million actually in circulation. Any contract on Asset Hub that prices dotUSD through the same supply cap is still reading a false number. The pool is nearly empty, so there is little left to lose there, but the figure needs a governance correction before dotUSD is safe to build on.

The outstanding actions, in order:

dotUSD was meant to be a serious piece of Polkadot infrastructure, a native Treasury-backed stablecoin. In its third week it showed that its supply could be deleted with a bridge message and a dollar, using a fix that had been sitting in the repository the whole time.

Sources

Everything above is public and verifiable.