The regression test the Fellowship added for the dotUSD incident fits on one screen. Alice holds 1,000 dotUSD and no DOT. She sends one WithdrawAsset for her dotUSD plus 1 DOT. dotUSD sorts first and gets withdrawn, the DOT withdrawal fails, the instruction errors. Before the fix, her balance came back and the supply didn’t.
Parity’s incident review went up yesterday (review) and explains it at a high level. I wanted to see both halves in code, the supply that drifted and the pool that priced it, so I read the fix in polkadot-sdk and that test (tests.rs at 7b27d43). All of it is fixed and public, and I’m only looking at the code here, not at the disclosure timeline.
The credit that dropped too late
Since polkadot-sdk PR #10384 (merged 16 February 2026) the XCM executor keeps real imbalances in holding instead of plain numbers. That PR was actually written to keep total issuance correct during XCM programs. WithdrawAsset takes each asset from the origin as a Credit and collects them in a local called withdrawn.
withdrawn was declared before TransactionalProcessor::process(|| ...) and only moved into holding in an .and_then after it. So when a later asset in the same instruction failed, the closure returned an error, the storage layer undid the earlier debits, and then withdrawn went out of scope with the credits still inside. Outside the layer.
A fungibles Credit is not inert. frame_support describes it as an imbalance “implying that the total_issuance value is greater than the sum of all account balances”. When one is dropped, pallet-assets runs DecreaseIssuanceWithEvent, which lowers supply and emits BurnedCredit. Inside the layer that write would have been rolled back together with the debit. Outside it, it stayed. Repeat the instruction and the recorded supply keeps falling while no balance moves.
The fix (PR #11910, merged 25 June) moves withdrawn inside the closure, so the credit drops while the layer can still undo it. The messages in the attack came over Snowbridge, but the review says they reproduced it without the bridge, and the test runs a local XCM, so the bridge is not really the story.
Why the pool cared
A wrong supply on its own is bookkeeping. What turned it into a price is one line in pallet-assets. reducible_balance ends with Ok(amount.min(details.supply)), and asset-conversion reads pool reserves through reducible_balance. So the dotUSD reserve the pool used was the smaller of what it held and the total supply.
That cap looks harmless, no account should hold more than the whole supply. But once supply fell under the pool’s real balance, the pool priced a smaller reserve than it had, dotUSD looked scarce, and it paid too much DOT for it. With the PSM minting dotUSD 1:1 against USDT, the trade was easy. It’s the same shape as the donation bugs in EVM vaults, where a recorded total and a real balance drift apart and the price follows the wrong one.
Fellows PR #1334 shows the size. At block 22326776 its remote test counted 4,213,885 dotUSD in balances across 63 accounts, against a recorded supply of 153,884. There is no extrinsic that sets supply, so the correction had to be a multi-block migration, shipped in runtime 2.5.2 through OpenGov.
The review also says the bug was rated medium in June, before dotUSD and its pool existed. That rating fit what read supply in June. By October a pool read it.
What I’d look for in other runtimes
Any imbalance (Credit, Debt, or a struct that owns one) created inside a with_transaction or TransactionalProcessor closure but owned by a variable outside it. If the closure can fail, the drop handler runs after the rollback and its write sticks. It’s easy to miss in review because a drop doesn’t look like a write. The test for it is the one above: make the last asset in a multi-asset operation fail, then assert that total_issuance didn’t move.
Then the list of everything that reads your supply. Here a safety cap made supply a price input without anyone writing a price feed, and pools, lending markets and share math built on reducible_balance all inherit that cap. For an asset with a few dozen holders, recomputing the sum of balances off-chain and comparing it to supply is cheap. A BurnedCredit on a stablecoin is also something I’d want an alert on.
What do you run today that would tell you an asset’s recorded supply stopped matching its balances?