ULME Approval Drain Exploit
Exploit Transactions
0xdb9a13bc970b97824e082782e838bdff0b76b30d268f1d66aac507f1d43ff4edVictim Addresses
0xae975a25646e6eb859615d0a147b909c13d31fedBSC0x55d398326f99059ff775485246999027b3197955BSCLoss Breakdown
Similar Incidents
MetaPoint Public Approval Drain
42%SwapX Arbitrary transferFrom Approval Drain on BNB Chain
40%FiberRouter Allowance Reuse Drain
35%Sareon Reward Drain
35%Eterna Buyback Treasury Drain
34%GGGTOKEN Treasury Drain via receive()
34%Root Cause Analysis
ULME Approval Drain Exploit
1. Incident Overview TL;DR
On BSC block 22476696, transaction 0xdb9a13bc970b97824e082782e838bdff0b76b30d268f1d66aac507f1d43ff4ed exploited the UniverseGoldMountain token (ULME, 0xae975a25646e6eb859615d0a147b909c13d31fed) by abusing its public buyMiner(address user,uint256 usdt) entry point. The attacker borrowed USDT from seven public DODO pools, bought ULME first, then forced 100 unrelated wallets that had approved the ULME contract to spend their USDT through buyMiner, which pushed additional USDT into the ULME/USDT Pancake pair and inflated the price long enough for the attacker to sell into the pump. The transaction ended with a direct profit transfer of 50646945323963576406725 raw USDT units to the attacker sender address.
The root cause is straightforward access-control failure in buyMiner: the function accepts an arbitrary user argument and immediately spends that user’s approved USDT with transferFrom without checking msg.sender == user or any signed delegation.
2. Key Background
UniverseGoldMountain is the ULME token on BSC. Its verified source shows that the contract stores a router address and a USDT token address via setRoter, approves that router to spend the contract’s USDT, and exposes buyMiner as a public function.
The relevant market is the ULME/USDT Pancake pair at 0xf18e5ec98541d073daa0864232b9398fa183e0d4. buyMiner takes USDT from a supplied wallet, swaps that USDT into ULME through the configured router path, and routes the acquired ULME through the transfer router at 0x25812c28CBC971F7079879a62AaCBC93936784A2 before returning the tokens to the ULME contract. In practice, this means the caller can turn someone else’s USDT approval into buy pressure on the ULME/USDT pair.
The seed artifacts show the attacker used only public infrastructure: DODO flashloan pools for temporary USDT liquidity, PancakeSwap liquidity for the buy and sell legs, and the public ULME contract entry point for the forced-buy stage.
3. Vulnerability Analysis & Root Cause Summary
The vulnerability class is an authorization failure that converts ordinary ERC-20 approvals into permissionless third-party spend rights. In the verified source, buyMiner is declared public, accepts an arbitrary user, increases the nominal amount by 10%, and then executes IERC20(_usdt_token).transferFrom(user,address(this),usdt) with no caller validation. Because the spender is the ULME contract itself, any wallet that had approved the ULME contract for USDT became drainable by any external caller.
That unauthorized spending does more than steal funds directly. The function immediately swaps the stolen USDT into ULME, so each victim wallet is involuntarily converted into AMM buy pressure. The attacker exploited that property by buying ULME first, triggering many forced buys second, and selling the pre-bought ULME after the pair price had been pushed upward. The economic payoff therefore depends on the same missing authorization check that enabled the drains.
4. Detailed Root Cause Analysis
The critical victim-side code path in the verified ULME source is:
function buyMiner(address user,uint256 usdt) public returns (bool) {
address[] memory token = new address[](2);
token[0] = _usdt_token;
token[1] = address(this);
usdt = usdt.add(usdt.div(10));
require(IERC20(_usdt_token).transferFrom(user, address(this), usdt), "buyUlm: transferFrom to ulm error");
uint256 time = sale_date;
sale_date = 0;
address k = 0x25812c28CBC971F7079879a62AaCBC93936784A2;
IUniswapV2Router01(_roter).swapExactTokensForTokens(usdt, 1000000, token, k, block.timestamp + 60);
IUniswapV2Router01(k).transfer(address(this), address(this), IERC20(address(this)).balanceOf(k));
sale_date = time;
return true;
}
The explicit invariant is that only the token owner, or a caller explicitly authorized by that owner, should be able to trigger spending from that owner’s USDT balance. The first code-level breakpoint is the transferFrom(user, address(this), usdt) call above, because user is attacker-controlled and msg.sender is never authenticated against it.
The seed trace for transaction 0xdb9a13bc... shows that this bug was exercised repeatedly by the attacker helper contract 0x8523c7661850d0da4d86587ce9674da23369ff26. The trace contains consecutive UniverseGoldMountain::buyMiner(...) calls for many unrelated wallets, each followed by BEP20USDT::transferFrom(victim, UniverseGoldMountain, amount) and then a transfer of that USDT from ULME into the Pancake pair. For example, the sample victim 0x4a005e5e40ce2b827c873ca37af77e6873e37203 is called through buyMiner and ends the transaction effectively drained.
The balance-diff artifact confirms the same mechanism at the state level. The sample victim’s USDT balance moved from 168374096153206112300685 to 2, and many additional approved wallets show the same pattern. At the pair level, the ULME/USDT pool’s USDT balance increased by 200170825418687386914428 raw units, which is consistent with the forced-buy stage injecting stolen USDT into the AMM before the attacker exit.
5. Adversary Flow Analysis
The sender EOA was 0x056c20ab7e25e4dd7e49568f964d98e415da63d3, and the execution target was attacker helper contract 0x8523c7661850d0da4d86587ce9674da23369ff26. The seed metadata shows the tx input was sent directly to that helper contract.
The on-chain sequence is:
- The helper contract nested seven DODO flashloans to assemble temporary USDT liquidity.
- Before touching victim approvals, the helper swapped
1000000000000000000000000raw USDT units into5232967282043181969272670raw ULME units through PancakeRouter. - The helper iterated through 100 approved wallets and called
ULME.buyMiner(victim, derivedAmount)for each. Each successful call triggeredUSDT.transferFrom(victim, ULME, amount)and then routed the stolen USDT into the ULME/USDT pair as additional buy pressure. - After the forced buys, the helper sold nearly all ULME back into USDT, repaid the flashloans, and transferred the remaining USDT profit to the sender EOA.
The trace’s closing segment shows the final ULME sell, flashloan repayments, and a concluding USDT::transfer to 0x056c20ab7e25e4dd7e49568f964d98e415da63d3. The balance diff records the sender’s direct USDT delta as +50646945323963576406725.
6. Impact & Losses
The exploit drained USDT from 100 third-party wallets that had approved the ULME contract as spender. The reported aggregate loss is 250817770742650963321153 raw USDT units with 18 decimals, and the transaction-level seed artifacts show multiple wallets ending at exactly 2 raw units after the exploit.
The attacker’s realized direct profit was 50646945323963576406725 raw USDT units, while the pair’s USDT reserve increased materially during the forced-buy phase. The impact therefore includes both direct theft from approved users and market manipulation of the ULME/USDT pool using the stolen funds.
7. References
- Seed transaction metadata for
0xdb9a13bc970b97824e082782e838bdff0b76b30d268f1d66aac507f1d43ff4ed. - Seed execution trace showing nested flashloans, repeated
buyMinercalls, victimtransferFromevents, the final ULME sell, and the final USDT profit transfer. - Seed balance diff showing the pair reserve increase, sample victim drain to
2, and attacker sender profit. - Verified UniverseGoldMountain source at
0xae975a25646e6eb859615d0a147b909c13d31fed, especiallysetRoterandbuyMiner.