On 12 September 2026, between 17:19 and 17:35 UTC, an attacker drained the MetalX AMM contract
proton.swaps of roughly 2.1 billion XPR plus its token reserves — one of the largest single
incidents in XPR Network's history. Within hours, block producers and Metallicus had frozen the funds,
and over the following day recovered about 87% to a governance-controlled treasury.
This is the complete, on-chain account: how the exploit worked, where the money went, how the recovery actually happened (including a second, unrelated bug that nearly stopped it), and — the part that matters most for the network's future — the two contract fixes we've submitted upstream so the same class of failure can't recur.
Everything below is reproducible from public chain history. We've cross-checked it against two independent forensic write-ups (Alvosec and others); the on-chain facts line up to the digit.
TL;DR
- ~2.1B XPR drained from
proton.swaps(~$5.4M at drain-time prices): ~1.556B directly, plus 563.5M borrowed fromlending.loanagainst stolen stablecoin collateral. - ~1.85B XPR (87%) recovered to a BP/Metallicus-controlled treasury, plus 100% of the stolen stablecoin collateral (1.37M XUSDC + 571K XMD) and the METAL/ecosystem-token reserves.
- ~277M XPR + 310K METAL escaped to KuCoin, MEXC and Gate.io before the freeze — now the subject of law-enforcement action, with strong KYC leads.
- The recovery needed a second bug fixed first:
eosio.wrap, the BP account-recovery tool, was broken. - We've shipped two permanent fixes to
XPRNetwork/proton.contracts: an inlineeosio.wrap(PR #8) and token-level freeze enforcement ineosio.token(PR #9).
1. The bug: one missing check
proton.swaps let users deposit tokens, track an internal balance, and withdraw. The withdraw action
was supposed to check "does this account have at least this much?", subtract it, and send the tokens.
The check was written as "balance ≥ requested amount." Nobody wrote "and the requested amount is greater than zero."
Because an Antelope asset carries a signed 64-bit amount, a negative quantity is perfectly valid
input. A negative amount sails through a balance ≥ amount guard (zero is greater than any negative
number), and then — because subtracting a negative adds — it inflates the attacker's internal
balance out of thin air. A follow-up positive withdraw then pays out the contract's real reserves.
The whole drain was a single atomic transaction
(1fee7e10eb0e…,
block 403,087,440): a negative withdraw of -1,526,084,924.3129 XPR immediately followed by a positive
one of the same size.
This wasn't the first attempt. Three months earlier, on 18 June 2026, an account named
gatedepostf ran the exact same negative-withdraw against proton.swaps — but with the wrong decimal
precision (-10000 XPR instead of -10000.0000 XPR), so it failed on a symbol mismatch. A public probe
that found the door, tried the handle wrong, and left. Someone else came back in September and turned it.
2. The drain and the money trail
The attacker's account, nobreed (created that morning, funded with 12,300 XPR from a KuCoin withdrawal),
drained ~1.556B XPR plus every pool token, then used the stolen XUSDC/XMD as collateral on lending.loan
to borrow another 563.5M XPR — squeezing maximum liquid value out of the position.
From there the funds fanned out through purpose-built mule accounts (sal2i4qb5byz, unionchainpi,
unionchainpo) toward exchanges. A second, independent exploiter — winterbloom4 — hit the same bug
hours later and laundered its share through the Alcor DEX.
While the pool was insolvent, it kept quoting as if nothing was wrong: 19,921 swaps executed against phantom reserves in the four-hour window between the drain (17:24) and the emergency pause (21:32) — a reminder that a drained AMM doesn't stop trading on its own.
The cover image above is the shape of it: one source, a fan-out of mules, exchange endpoints on the right, and — in teal — the recovery converging back into a single controlled treasury.
3. Where the money went
| Destination | Amount | Status |
|---|---|---|
Recovery treasury (recoverfunds) | ~1.28B XPR + 12.87M METAL + 1.37M XUSDC + 571K XMD + wrapped BTC/ETH/DOGE + illiquid ecosystem tokens | ✅ secured |
lending.loan (debt repaid) | 563.6M XPR | ✅ protocol made whole |
KuCoin (kcdeposit4in) | ~269.4M XPR | ⚠️ escaped — KYC lead |
MEXC (mexcloanloan) | 7.5M XPR + 310K METAL | ⚠️ escaped — KYC lead |
Gate.io (gatedeposit) | 33 XPR (test) | ⚠️ escaped — KYC lead |
On the incident day, the attacker's deposits were 99.7% of all XPR that hit KuCoin's deposit wallet — not a needle in a haystack, they were the haystack. Both exploiters reused the same KuCoin deposit memo (a unique customer ID), which ties them to a single exchange account — the strongest deanonymization lead of the whole case. Exchange freeze/KYC requests and law-enforcement filings are underway for the escaped ~277M.
4. The recovery — and the bug that almost blocked it
Block producers froze the attacker cluster within hours using the network's actor-blacklist. But seizing
compromised accounts requires eosio.wrap — the privileged contract BPs use to execute recovery actions
by multisig — and eosio.wrap was broken.
Its exec scheduled the recovery as a deferred transaction. Producers running modern Antelope
(Leap 5+/Spring) no longer process deferred transactions at all, so the first recovery multisigs
"executed" successfully… and then silently did nothing.
The fix was to make eosio.wrap run the wrapped actions inline instead of deferring them — matching
the AntelopeIO reference contracts. We wrote the patch, built it, tested it on testnet, and BPs deployed
it to mainnet by multisig. With a working recovery tool, the block producers then:
- Rotated the keys on the attacker accounts to a governance-held recovery key.
- Repaid the 563.6M XPR
lending.loandebt and redeemed 100% of the stolen stablecoin collateral. - Swept the remaining balances into the
recoverfundstreasury.
Net result: ~1.85B XPR (87%) and all the stablecoin collateral secured. The ~13% that escaped had already reached centralized exchanges before the freeze.
5. Attribution
On-chain analysis shows the attacker accounts weren't random — they're a coordinated cluster. Several infrastructure/mule accounts were created months in advance by the same cryptographic key, and stolen funds were swept between them under fake "order ID" memos to look like a payment processor. That key linkage, plus the shared exchange deposit IDs, ties the operation to a single actor.
We're deliberately not naming individuals here. Attribution is an exchange-KYC and law-enforcement matter, and the leads are strong: the seed funding, the cash-outs, and the deposit memos all point back to identifiable exchange accounts.
6. The permanent fixes
Recovering the funds was the emergency. Making sure the network is structurally better afterwards is the job. This incident exposed two gaps, and we've submitted upstream fixes for both:
1. eosio.wrap — inline execution
(PR #8).
The account-recovery tool was left on the pre-Leap-5 deferred implementation — broken before it was ever
needed. The emergency mainnet patch is now a proper, reviewed, testnet-verified pull request against
XPRNetwork/proton.contracts, so the canonical eosio.wrap works on modern Antelope. Privileged system
contracts should never be a surprise dependency in the middle of an incident.
2. eosio.token — enforce the network blocklist
(PR #9).
XPR already has a shared on-chain blocklist contract that the wrapped tokens (xtokens) honour — a
blocklisted account can't move METAL, XUSDC, etc. But eosio.token (native XPR) never checked it — the
token contract has been unchanged since December 2020. So stolen XPR could keep moving even while the
same account's wrapped balances were frozen. PR #9 makes eosio.token read the same shared blocklist
(sender-side, exactly matching xtokens), with core system accounts explicitly exempt so a blocklist entry
can never halt staking, RAM, or rewards chain-wide. We built it, and verified the full freeze/unfreeze flow
end-to-end on testnet.
Both are drafts pending human review and audit before any mainnet deployment — as privileged system-contract changes should be.
7. What proton.swaps needs (and what everyone can learn)
- Assert
quantity.amount > 0on every token-moving action. Abalance ≥ amountguard is not a positive-amount check; signed asset amounts make negative inputs a real attack surface. Audit every deposit/withdraw ledger for signed-amount handling. - Invariant tests: a contract's real token balance must always equal the sum of its internal ledger balances. That single test would have caught this in CI.
- A fast circuit-breaker. A four-hour gap between drain and pause is a lot of phantom swaps. Emergency pause should be reachable quickly, not only through a slow multisig.
- Keep privileged system contracts current.
eosio.wrapbeing on a deprecated deferred model turned a bad day into a much longer one.
Closing
No user lost funds to a stolen private key here — this was a contract bug, and the response was a coordinated block-producer and Metallicus effort that got most of the value back and is now closing the gaps that made it possible. That's the system working: fast containment, honest post-mortem, and permanent fixes in the open.
If you build on XPR Network and want a second set of eyes on your contracts before they hold real value, that's exactly the kind of review this incident argues for. Reach out.
Transaction IDs, account names, and figures in this article are verifiable on explorer.xprnetwork.org. ProtonNZ is an XPR Network block producer.
