Loss: roughly $25M was lost across the combined same-transaction xSNXa + xBNTa exploit, while xToken’s initial report said the xSNXa contract itself lost 416 ETH directly and the Balancer xSNXa pool was also drained
Summary: xToken’s xSNX.mint() path sometimes priced fresh SNX from the attacker’s own just-executed ETH→SNX swap rather than from an independent oracle. When allocateToEth == false, TradeAccounting.calculateTokensToMintWithEth() set weiPerOneSnx = ethContributed / snxContributed, where snxContributed was simply the balance delta after Kyber routed the purchase. The attacker used a large flash-loan-funded SNX dump to crater SNX’s market price on the route xToken relied on, then minted an outsized amount of xSNXa for almost no ETH because the contract interpreted a very large amount of SNX acquired for tiny ETH as SNX is extremely cheap in ETH terms, which collapsed the fund’s issue-side NAV and let the attacker buy wrapper supply far below the value it could later command on exit. The attacker then cashed that overminted supply out through two healthier rails: direct redemption against the contract’s real ETH reserve and sales into the Balancer SNX/ETH/xSNXa pool. The decisive failure was not a generic thin-token collateral pump; it was that issuance trusted an attacker-shaped trade result while exit liquidity still honored a different valuation boundary.
Trigger / entry point:
The vulnerable entrypoint was xSNX.mint(uint256 minRate), which accepted ETH, optionally swapped it to SNX through kyberNetworkProxy.swapEtherToToken(...), and then asked TradeAccounting.calculateTokensToMintWithEth(...) how many xSNXa to mint.
In the branch where the strategy wanted more SNX rather than more ETH hedge inventory, the mint math did not use Synthetix’s oracle-derived SNX/ETH price.
Instead, calculateTokensToMintWithEth(...) derived weiPerOneSnx from ethContributed / (snxBalanceAfter - snxBalanceBefore), so the mint price depended on the exact execution quality of the attacker-influenced swap.
The redemption side still used calculateRedemptionValue(...), which valued the position from a different path: oracle-style SNX pricing plus the contract’s live ETH reserve.
Separately, Balancer still offered an external pool where the newly minted xSNXa could be dumped for real SNX and ETH.
Root cause:
xToken let the issuance side self-price new SNX inventory from the same manipulated trade that supplied it.
That made the mint boundary depend on a venue the attacker could distort with borrowed capital in the same transaction.
The protocol therefore split one economic promise across two inconsistent pricing rules: issuance honored the attacker’s local execution price, while redemption and pool exit still honored materially firmer value rails.
Once those two sides diverged, flash-loan market manipulation became a mint primitive rather than merely a bad oracle input.
Exploit path:
The attacker opened a large dYdX flash loan of roughly 61,833 ETH.
They used part of that capital to borrow about 564,000 SNX on Aave and buy roughly 700,000 SNX on SushiSwap, assembling inventory for an aggressive SNX selloff.
They sold about 1.2M SNX on Uniswap V2, sharply worsening the ETH→SNX execution price on the route family xToken relied on through Kyber.
With SNX artificially cheap in the venues feeding the Kyber-routed purchase and the xToken mint path observing only that attacker-shaped cheap-SNX execution result, they called xSNX.mint() and reportedly minted about 1.2B xSNXa for only about 0.12 ETH of marginal cost on that step.
They burned roughly 105M xSNXa back into the contract and pulled out about 414 ETH from xToken’s real ETH reserve.
They sold additional overminted xSNXa into the Balancer SNX/ETH/xSNXa pool, draining external pool liquidity that still treated the token as valuable.
They then reversed the preparatory SNX trades enough to unwind loans and later continued within the same transaction into the separate xBNTa exploit branch.
What the attacker needed to be true:
xSNX.mint() had to be in the allocateToEth == false branch so ETH was swapped into SNX and the contract inferred price from swap output.
The Kyber-routed execution price had to be movable enough, relative to attack capital, that snxContributed could be artificially inflated inside the mint calculation.
The attacker needed healthier exit rails than the manipulated mint rail: redeemable ETH inside the contract and/or a sufficiently liquid external Balancer pool.
The entire sequence needed to fit atomically so the attacker could print, cash out, and unwind market setup before normal arbitrage repaired the price.
Technical anatomy worth remembering:
The key line was not spot oracle was manipulable; it was mint math used the attacker's own trade result as the effective oracle.
calculateTokensToMintWithEth(...) and calculateRedemptionValue(...) did not share one consistent same-state pricing source. That asymmetry made mint -> redeem/sell profitable without any lasting improvement in the fund’s real backing.
This is a useful wrapper / fund analogue to later vault incidents where issuance uses one accounting truth and redemption uses another. The attacker prints claims under one local fiction and exits through a still-honest reserve or market.
The case is also a reminder that aggregator routing does not create oracle quality. A routed trade result is still just execution on manipulable venues if no independent valuation boundary constrains how that result feeds minting.
Representative code and artifacts:
Vulnerable mint entrypoint: contracts/xSNX.sol (mint(uint256 minRate))
Vulnerable mint-pricing branch: contracts/TradeAccounting.sol (calculateTokensToMintWithEth(...), especially the allocateToEth == false branch deriving weiPerOneSnx from ethContributed / snxContributed)
Redemption pricing path: contracts/TradeAccounting.sol (calculateRedemptionValue(...) and getWeiPerOneSnxOnRedeem())
xToken paused minting across all contracts shortly after the incident.
The team published an initial report separating the xSNXa and xBNTa mechanics and stated that most SNX remained in xToken’s core contracts even though liquid pool and ETH-reserve value had been lost.
xToken later snapshotted affected xSNXa holders and LPs and distributed replacement tokens for recovery.
The enduring mitigation lesson is not only use better oracle inputs; it is do not let the issuance path define NAV from the attacker's own execution result if redemption or secondary-market exits still honor a different notion of value.
Three issue-side valuation promotions this incident collapsed
1. A routed ETH -> SNX execution result could still be attacker-shaped
xToken’s own initial report preserved the first important local fact plainly: when minting xSNXa in the ETH is converted to SNX branch, the contract compared the user’s ETH contribution to the amount of SNX acquired and used that ratio for valuation purposes.
The archived TradeAccounting.sol code makes that exact boundary concrete. In calculateTokensToMintWithEth(...), when allocateToEth == false, the contract read snxContributed = snxBalanceAfter - snxBalanceBefore and then set weiPerOneSnx = ethContributed * 1e18 / snxContributed.
At that stage, the only true claim should have been this one routed purchase returned this much SNX for this much ETH under current execution conditions. That is still just a fresh, venue-shaped trade result, not yet a money-good statement about reserve-backed wrapper value.
2. xToken still promoted that fresh route result into canonical issue-side NAV truth
The decisive second promotion was that xToken did not stop at recording how the trade executed. It fed that attacker-shaped weiPerOneSnx straight into calculateIssueTokenPrice(...), which in turn defined how many new xSNXa claims the incoming ETH deserved.
The asymmetry inside the code matters. The allocateToEth == true branch used getWeiPerOneSnxOnMint() and therefore the Synthetix-style oracle path, but the exploited allocateToEth == false branch bypassed that oracle and treated the route output itself as the SNX/ETH valuation boundary.
This is the sharper corpus lesson worth preserving: the trade filled, that fill now counts as canonical issue-side NAV, and the supply minted from that NAV is safe to honor elsewhere were different promotions, but xToken collapsed the first two into one calculation.
3. That published mint-time truth still inherited healthier reserve and pool-exit authority
The exploit only paid because freshly overminted xSNXa did not remain trapped inside the same manipulated pricing boundary. The attacker could immediately move those claims onto firmer exit rails.
xToken’s own report said the attacker sold the disproportionate xSNXa supply for real SNX and ETH on the Balancer pool, and that 416 ETH was extracted directly from the xSNXa contract. The code-side reason is also visible: redemption used calculateRedemptionValue(...) and getWeiPerOneSnxOnRedeem(), which valued SNX from the separate redeem-side pricing path plus the contract’s live ETH reserve rather than from the attacker’s just-executed mint trade.
That is the full authority ladder the note should keep visible: one routed trade can be manipulated, xToken promotes that manipulated fill into issue-side NAV, and the resulting wrapper supply still deserves healthier redemption and secondary-market exit authority were three different promotions, and the exploit worked because xToken treated them as if they described one coherent value object.
xToken let one attacker-shaped routed trade result do work that should have belonged to a slower or more independent valuation boundary. The protocol did not merely read a thin external spot price; it turned its own just-executed Kyber fill into issue-side NAV.
The mint path and the cash-out paths did not honor one coherent reserve-backed claim object. calculateTokensToMintWithEth(...) priced fresh supply from manipulated execution quality, while redemption math and Balancer liquidity still respected materially firmer value rails.
Because issuance was cheap exactly when redemption and pool exits remained rich, the protocol created an attacker-owned spread between mint cost and exit value.
The dangerous object was not SNX price in the abstract. It was xToken’s own published interpretation of how much SNX this ETH just bought, immediately promoted into mint authority without a second check on realizability or round-trip boundedness.
The flash-loan setup mattered because it made the venue distortion large and atomic, but the architectural failure was already present before the attacker borrowed size.
Detection and audit pivots
Reconstruct the full ETH -> routed SNX purchase -> xSNXa mint -> burn / Balancer sale loop and ask whether any leg uses a different truth object for value than the others. If issue-side math trusts route execution while exit-side math trusts reserve-backed value, the spread is already suspect.
Compare attacker-shaped execution quality with the protocol’s own reserve and redemption accounting. The important question is not just can the route be manipulated? but can manipulated route output still mint claims that other paths will honor at a healthier mark.
Test same-transaction mint -> redeem and mint -> dump into external pool loops at flash-loan scale. If the protocol’s own freshest internal mark can immediately reach firmer reserve or secondary-market rails, the publication boundary is too weak.
Inventory every place wrapper supply can cash out: protocol redemption, AMM pools, vault exits, lending collateral, or treasury buybacks. A self-priced mint is only catastrophic when at least one other venue still pays for the wrapper more honestly.
Ask whether routed execution is being treated as a quote, a receipt proof, or a valuation oracle. Aggregator fills are often safe as settlement mechanics and unsafe as issuance authority.
Failure-prevention checklist
Keep issuance and redemption on one bounded round-trip truth object. If ETH in -> SNX acquired -> xSNXa minted -> xSNXa redeemed does not preserve one conservative solvency story, the protocol is manufacturing an exploitable spread.
Quarantine route-derived marks before they become mint authority. A fresh aggregator fill, same-block reserve read, or attacker-shaped acquired-amount ratio should not immediately define redeemable wrapper supply at full size.
Separate execution quality from NAV authority. Routing helpers can decide where to buy SNX without deciding how much reserve-backed wrapper value that purchase mints.
Cap same-transaction issue-side price moves and same-transaction round trips. If one routed trade can collapse mint cost, the protocol should not also allow immediate redemption or deep external-pool monetization against the resulting supply.
Force reserve-paying consumers to re-check realizability. Redemption, treasury, or other reserve-backed exit paths should not honor fresh wrapper supply at face value when the minting mark came from attacker-shapeable execution.
Treat external wrapper liquidity as part of the security boundary. If Balancer or another AMM can pay out healthier value than the mint path charged, that pool must be considered an exploit rail, not an incidental market.
Add flash-loan-scale simulations around route-sensitive mint math, especially when the protocol acquires assets through a DEX aggregator and mints immediately spendable claims from the received amount.
Audit prompts
Which exact object determines issue-side NAV here: oracle price, routed execution amount, reserve ratio, basket estimate, or some blend of them?
Can the same transaction both worsen route execution, publish that worsened execution as a mint-time price, and then cash the resulting wrapper supply out against a firmer redemption or AMM rail?
What invariant limits the best possible mint -> redeem or mint -> dump round trip when the route used to acquire backing inventory is itself manipulable?
If a routed purchase returns far more SNX than normal for this ETH, what exact rule prevents the protocol from reading that as wrapper shares should be extremely cheap to mint?
Which downstream consumers still honor the wrapper at a different valuation boundary: redemption contracts, AMM pools, lending markets, or treasury operations?
Does the protocol distinguish we successfully acquired inventory from that acquired amount deserves immediate full reserve-backed mint authority?
If the route’s execution quality moves violently in one block, what mechanism narrows minting, redemptions, or external-liquidity exposure before reserves are drained?
Lessons that should already be standard
A routed fill is not an oracle just because the protocol itself executed it.
Issue-side pricing and reserve-paying exit-side pricing must share one conservative solvency story, or attackers will own the spread between them.
Assets were acquired is not yet the same claim as those assets justify this much immediately redeemable wrapper supply.
Secondary-market liquidity is part of the mint-security model. A self-priced wrapper can still be drained through an AMM even if direct protocol redemption is bounded.
Flash loans usually scale this class of bug; they do not create it. The deeper mistake is letting one atomic local-state print become immediate claim-creation authority.
The safer design question is not only can this route be manipulated? but what exact gate stops route execution quality from becoming reserve-backed mint authority right now.
Why this incident matters
It is a clean preserved example of self-priced issuance: the protocol effectively asked the attacker, via trade execution, what SNX should be worth for minting.
It sits on the boundary between oracle manipulation and issuance/redemption asymmetry, which makes it useful as a bridge exemplar between those two pattern notes.
It also shows why we only use execution output, not an external oracle is not a defense. If execution can be attacker-shaped and mint math trusts it directly, the execution venue is the oracle.
Although the public story often bundles xSNXa and xBNTa together as one xToken exploit, preserving the xSNXa branch separately is worthwhile because its failure mode is a distinct implicit price from manipulated acquired amount pattern rather than the path-validation bug that broke xBNTa.
Sharper self-authored-mark lens: protocol-authored-market-state-oracle-failure — the mint path did not just consume a bad external venue quote; xToken’s own accounting logic hardened attacker-shaped Kyber execution into issue-side NAV and then let that locally published mark mint immediately spendable wrapper supply.
Positive-design companion: internal-mark-publication-quarantine-and-reserve-spend-separation — xToken let a protocol-authored interpretation of attacker-shaped swap execution become immediate mint authority, then let the resulting wrapper supply reach real reserve and pool-exit rails without any quarantine step.
Reserve-bounded issuance companion: reserve-backed-claim-truth-security-object — useful when the sharper question is what exact intake / issue / redeem object should have kept Kyber execution quality, minted xSNXa quantity, and downstream redemption liability on one coherent rail.
Adjacent wrapper / LP valuation family: lp-token-oracle-mispricing — not because xSNXa was an LP token, but because a fund-like wrapper turned a manipulable internal valuation step into redeemable external value.
Control surface
The decisive leverage sat in the mint-time pricing boundary: Kyber-routed ETH→SNX execution fed directly into calculateTokensToMintWithEth(...), while the attacker still had access to healthier exit rails through burn() and Balancer liquidity.
The dangerous promise was not merely this fund prices SNX badly. It was a manipulated acquisition price can define how many redeemable wrapper tokens fresh ETH deserves.