# How a dropped XCM credit lowered dotUSD supply, and why the pool priced it

**URL:** <https://forum.polkadot.network/t/how-a-dropped-xcm-credit-lowered-dotusd-supply-and-why-the-pool-priced-it/19209>\
**Category:** Tech Talk\
**Created:** [November 3, 2026, 4:46am UTC](https://forum.polkadot.network/t/how-a-dropped-xcm-credit-lowered-dotusd-supply-and-why-the-pool-priced-it/19209 "2026-11-03T04:46:35Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![Zealynx](https://avatars.discourse-cdn.com/v4/letter/z/e480ec/32.png) [@Zealynx](https://forum.polkadot.network/u/Zealynx)\
**Post date:** [November 3, 2026, 4:46am UTC](https://forum.polkadot.network/t/how-a-dropped-xcm-credit-lowered-dotusd-supply-and-why-the-pool-priced-it/19209/1 "2026-11-03T04:46:35Z")

</div>

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](https://forum.polkadot.network/t/xcm-incident-review-what-happened-and-what-we-learned/19207)) 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](https://github.com/polkadot-fellows/runtimes/blob/7b27d439d1393cf0cdf7ef09217720b820cf5bf8/system-parachains/asset-hubs/asset-hub-polkadot/tests/tests.rs#L1580-L1625)). 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?
