This is a lower bound: only assets with reliable historical USD prices are counted, so the actual loss may be higher.
0x5a504fe72ef7fc76dfeb4d979e533af4e23fe37e90b5516186d5787893c379910xa943ea143cd7e79806d670f4a7cf08f8922a454fBSC0xa08c4571b395f81fbd3755d44eaf9a25c9399a4aBSC0xdd0c4a96a43b36d91f4fedf83489b954c287886aBSC0x9b9bad4c6513e0ff3fb77c739359d59601c7caffBSC0x0fbb9456e929f66853973ba12109d9569d2d4966BSC0x04536fb62db86ccf3c7b9b9ddd6dc10fedc92148BSCOn BNB Chain block 6947154, transaction 0x5a504fe72ef7fc76dfeb4d979e533af4e23fe37e90b5516186d5787893c37991 used helper contract 0x2b528a28451e9853f51616f3b0f6d82af8bea6ae to drain five Uranium Finance liquidity pairs. The exploit was permissionless: every successful swap sent only one smallest unit of each pool token into a pair and still withdrew most of both reserves.
The root cause was a broken AMM invariant inside UraniumPair.swap(). Uranium changed the adjusted-balance fee scale from 1000 to 10000, but left the right-hand side of the constant-product check at reserve0 * reserve1 * 1000**2. That weakens the invariant by a factor of 100 and allows a caller to keep only about 10% of each reserve in the pool while the swap() call still succeeds.
Uranium Finance is a Uniswap V2 style AMM deployment. In this model, the pair contract must enforce a fee-adjusted constant-product invariant on every swap so that output amounts cannot exceed what the input amounts justify.
The relevant Uranium components were public and permissionless: factory 0xa943ea143cd7e79806d670f4a7cf08f8922a454f and the drained pairs 0xa08c4571b395f81fbd3755d44eaf9a25c9399a4a, 0xdd0c4a96a43b36d91f4fedf83489b954c287886a, 0x9b9bad4c6513e0ff3fb77c739359d59601c7caff, 0x0fbb9456e929f66853973ba12109d9569d2d4966, and . The adversary helper only automated repeated public calls; it did not introduce any privileged capability over those pools.
0x04536fb62db86ccf3c7b9b9ddd6dc10fedc92148One nuance is that RADS is deflationary on transfer. That affects the exact amount ultimately received by the attacker, but it does not change the pair-side failure: the pair still transfers out excessive reserves because the invariant check itself is numerically wrong.
The vulnerability class is an arithmetic invariant bug in the swap accounting path. Uranium modified fee math to use a 10000 scale in the adjusted balances but failed to update the invariant threshold to the same scale. As a result, the left-hand side of the invariant becomes 100 times easier to satisfy than intended. A caller can therefore send trivial nonzero inputs on both sides, request simultaneous large outputs from both reserves, and still pass the post-swap require.
The verified pair source shows the issue directly:
uint balance0Adjusted = balance0.mul(10000).sub(amount0In.mul(16));
uint balance1Adjusted = balance1.mul(10000).sub(amount1In.mul(16));
require(
balance0Adjusted.mul(balance1Adjusted) >=
uint(_reserve0).mul(_reserve1).mul(1000**2),
'UraniumSwap: K'
);
With a correct 10000-based denominator, a 90% withdrawal from both sides would revert. With the deployed code, retaining roughly 10% of each reserve is enough to satisfy the weakened check, so liquidity provider value can be extracted directly from the pools.
The exploit path is deterministic and fully reconstructible from public data. The seed transaction metadata identifies sender 0xc47bdd0a852a88a019385ea3ff57cf8de79f019d calling helper 0x2b528a28451e9853f51616f3b0f6d82af8bea6ae in block 6947154. Trace evidence shows that helper querying Uranium factory state and then repeatedly calling the five vulnerable pairs.
At code level, the safety invariant should preserve the same fee scale on both sides of the comparison. Uranium instead computes:
balanceAdjusted = balance * 10000 - amountIn * 16
but compares the result against:
reserve0 * reserve1 * 1000**2
That mismatch means the attacker can choose amount0Out and amount1Out so the pool retains only about one tenth of each reserve. Algebraically, (0.1 * reserve0 * 10000) * (0.1 * reserve1 * 10000) still clears reserve0 * reserve1 * 1000**2, modulo the tiny one-unit inputs needed to make amount0In and amount1In nonzero.
The seed trace and decoded logs match that mechanism exactly. Representative swap events show amount0In = 1 and amount1In = 1 paired with outsized withdrawals:
0xa08c4571...9a4a::swap(6223980431479130328487, 200885644505814114857210, helper, 0x)
emit Swap(sender: helper, amount0In: 1, amount1In: 1, amount0Out: 6223980431479130328487, amount1Out: 200885644505814114857210, to: helper)
0x0fBb9456...4966::swap(5100244547151102063440625, 5103572368303035056692684, helper, 0x)
emit Swap(sender: helper, amount0In: 1, amount1In: 1, amount0Out: 5100244547151102063440625, amount1Out: 5103572368303035056692684, to: helper)
Balance-diff evidence confirms that the reserve losses became attacker-controlled assets by transaction end. The helper gained 16212661679439638689624061 BUSD, 5661271447337723290419093 USDT, 79508708291234332225 BTCB, and 124518799516178812003866 RADS, while the corresponding pools lost matching quantities on their token sides.
The adversary flow had three stages. First, the attacker-controlled EOA invoked the helper contract. Auditor analysis tied the helper to that EOA through owner-slot evidence and owner-gated bytecode behavior observed in the execution trace.
Second, the helper iterated vulnerable Uranium pairs discovered through factory state and executed repeated swap sequences. For each targeted pair, it transferred one smallest unit of token0 and token1 into the pair, then called swap() requesting approximately 90% of each reserve.
Third, each successful swap updated reserves instead of reverting, leaving the pair with about 10% of prior balances and moving the withdrawn assets into the adversary-controlled helper. The traced sequence covered the RADS/BUSD, RADS/WBNB, WBNB/BUSD, USDT/BUSD, and BTCB/WBNB pairs in the same transaction.
The public on-chain flow can be summarized as:
EOA 0xc47bdd0a...f019d
-> helper 0x2b528a28...a6ae
-> query Uranium factory 0xa943ea14...454f
-> transfer 1 unit of each pair token into target pair
-> call target UraniumPair.swap(about 90% reserve0, about 90% reserve1, helper)
-> repeat across five vulnerable pairs
Because every step uses public contracts and ordinary swaps, any unprivileged actor with minimal token dust could realize the same opportunity against affected Uranium pairs.
The exploit drained most liquidity from five Uranium pools in a single transaction and exposed any similarly configured Uranium pair to the same permissionless strategy.
Measured losses from the validated root-cause artifact were:
RADS: 127059999506304910208030BUSD: 16212661679439638689624070WBNB: 25621067636796259996688USDT: 5661271447337723290419096BTCB: 79508708291234332228Those amounts are raw smallest-unit on-chain quantities. The blast radius was protocol-wide for any pair running the same broken invariant check.
0x5a504fe72ef7fc76dfeb4d979e533af4e23fe37e90b5516186d5787893c379910xa943ea143cd7e79806d670f4a7cf08f8922a454f0xa08c4571b395f81fbd3755d44eaf9a25c9399a4a, 0xdd0c4a96a43b36d91f4fedf83489b954c287886a, 0x9b9bad4c6513e0ff3fb77c739359d59601c7caff, 0x0fbb9456e929f66853973ba12109d9569d2d4966, 0x04536fb62db86ccf3c7b9b9ddd6dc10fedc92148UraniumPair.sol and UraniumFactory.sol