Unibot Approval Drain
Exploit Transactions
0xcbe521aea28911fe9983030748028e12541e347b8b6b974d026fa5065c22f0cfVictim Addresses
0x126c9fbab3a2fca24edfd17322e71a5e36e91865EthereumLoss Breakdown
Similar Incidents
USDTStaking Approval Drain
46%Flooring extMulticall Approval Drain
41%0x7CAE Approved-Spender Drain
37%TheNFTV2 Stale Burn Approval
37%V3Utils Arbitrary Call Drain
36%Dexible selfSwap allowance drain
36%Root Cause Analysis
Unibot Approval Drain
1. Incident Overview TL;DR
In Ethereum mainnet block 18467806, transaction 0xcbe521aea28911fe9983030748028e12541e347b8b6b974d026fa5065c22f0cf drained UNIBOT from 17 holders that had previously approved Unibot router 0x126c9fbab3a2fca24edfd17322e71a5e36e91865. The attacker EOA 0x413e4fb75c300b92fec12d7c44e4c0b4faab4d04 sent a normal EIP-1559 transaction to helper contract 0x2b326a17b5ef826fa4e17d3836364ae1f0231a6f, which then repeatedly invoked router selector 0xb2bd16ab.
The root cause is a router trust-boundary failure. The router accepted an attacker-controlled ERC20-like wrapper for balance validation while separately forwarding attacker-supplied calldata that reached the real UNIBOT token 0xf819d9cb1c2a819fd991781a822de3ca8607c3c9. That combination let the router consume victims' standing approvals and execute real transferFrom(victim, attacker, amount) calls without binding the spend to msg.sender or to the token that had just been "validated".
2. Key Background
UNIBOT token 0xf819d9cb1c2a819fd991781a822de3ca8607c3c9 is a standard ERC20. Its verified source shows normal allowance-backed transferFrom behavior: if the caller is an approved spender, it can move tokens from sender to recipient and decrement the stored allowance.
Snippet from the verified UNIBOT source:
function transferFrom(
address sender,
address recipient,
uint256 amount
) public virtual override returns (bool) {
_transfer(sender, recipient, amount);
uint256 currentAllowance = _allowances[sender][_msgSender()];
require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
unchecked {
_approve(sender, _msgSender(), currentAllowance - amount);
}
return true;
}
That matters because the exploit did not break the token contract. It abused the fact that router 0x126c9f... was already an approved spender for many holders. The router itself is unverified, but the collected trace and decompilation show its vulnerable path. The attacker helper only needed to emulate enough ERC20 surface area for the router's validation logic to accept the call flow.
The incident is ACT. The adversary needed only public information and permissionless actions: existing on-chain approvals, public token balances, a locally deployed helper contract, and a normal transaction.
3. Vulnerability Analysis & Root Cause Summary
The vulnerable component is Unibot router 0x126c9fbab3a2fca24edfd17322e71a5e36e91865, specifically selector 0xb2bd16ab and its downstream external-call path. The router treated two attacker-controlled inputs as if they were coherent: an ERC20-like contract used for balance checks, and calldata that could reach the real approved token. That broke the invariant that a router may only spend approvals belonging to the caller who initiated the action, and that post-trade token validation must refer to the same token whose allowance is being consumed.
The attacker helper demonstrates the bug. Its decompilation shows a transfer(address,uint256) path that simply returns true, and a balanceOf(address) path that proxies to the real token but substitutes the beneficiary's balance when queried on itself. In practice, that let the router observe "before" and "after" balances on the malicious wrapper while the real value movement happened directly on UNIBOT.
The core failure is therefore not in UNIBOT and not in the helper. It is in the router's inability to bind authorization, accounting, and the actual token transfer into one consistent object. Any holder with a standing approval to the router could be drained by an unprivileged attacker.
4. Detailed Root Cause Analysis
The exploit pre-state was Ethereum mainnet immediately before tx 0xcbe521aea28911fe9983030748028e12541e347b8b6b974d026fa5065c22f0cf. In that state, the victims held UNIBOT and had already granted large allowances to router 0x126c9f.... The attacker did not need signatures, stolen keys, or privileged roles.
The focused exploit trace shows the exact sequence on one sampled victim 0xa20cb17d888b7e426a3a7ca2e583706de48a04f3:
UnibotV2::balanceOf(0xA20Cb17D888b7E426A3a7Ca2E583706dE48a04f3) -> 1888305870016036424
UnibotV2::allowance(0xA20Cb17D888b7E426A3a7Ca2E583706dE48a04f3, 0x126c9FbaB3A2FCA24eDfd17322E71a5e36E91865) -> max_uint256
0x126c9F...::b2bd16ab(...)
0x2b326A...::balanceOf(router) -> 0
0x2b326A...::balanceOf(helper) -> 0
UnibotV2::transferFrom(victim, attacker, 1888305870016036424) -> true
0x2b326A...::balanceOf(router) -> 0
0x2b326A...::balanceOf(helper) -> 1888305870016036424
That trace proves four critical points:
- The victim had both balance and router allowance in the pre-state.
- The router used the malicious helper for balance checks.
- The actual asset transfer executed on the real UNIBOT token.
- The destination of the real transfer was the attacker EOA, not the router.
The helper decompilation explains why the router accepted this. Its transfer path always returns success, and its balanceOf path proxies to the real token while treating the helper's own address as an alias for the beneficiary:
function Unresolved_a9059cbb(address arg0, uint256 arg1) public pure returns (bool) {
return 0x01;
}
function Unresolved_70a08231(address arg0) public payable returns (uint256) {
address var_b = address(arg0);
(bool success, bytes memory ret0) = address(store_a / 0x01).Unresolved_70a08231(var_b);
return var_d;
var_b = address(store_b / 0x01);
(bool success, bytes memory ret0) = address(store_a / 0x01).Unresolved_70a08231(var_b, var_f);
return var_d;
}
Semantically, the helper makes balanceOf(helper) mirror the attacker's real UNIBOT balance. So after the router forwards UNIBOT.transferFrom(victim, attacker, amount), the router's post-check against the helper sees the "asset" balance increase even though the router never received UNIBOT and the declared token-like contract was not the token that actually moved.
The invariant break can be stated directly: a swap or execution router must not consume approvals from arbitrary third-party holders, and it must not validate outcomes against a token-like contract that is different from the token whose allowance is being spent. Selector 0xb2bd16ab violates both conditions.
5. Adversary Flow Analysis
The attacker strategy was a single-transaction, contract-assisted approval drain:
- EOA
0x413e4fb75c300b92fec12d7c44e4c0b4faab4d04submitted tx0xcbe521aea28911fe9983030748028e12541e347b8b6b974d026fa5065c22f0cfto helper0x2b326a17b5ef826fa4e17d3836364ae1f0231a6f. - The helper iterated over a list of victim addresses embedded in the transaction input.
- For each victim, the helper invoked router selector
0xb2bd16abwith crafted parameters so the router would:- query the helper for pre/post balances,
- forward attacker-controlled calldata to the real UNIBOT token,
- execute
UNIBOT.transferFrom(victim, attacker, victimBalance), - accept the call because the helper's reported balance changed in lockstep with the attacker's real UNIBOT balance.
- This repeated until all listed approved holders were drained.
No private orderflow, governance role, signer compromise, or helper-contract access control was required. The feasibility condition was simply: the victim had UNIBOT and had already approved the router.
6. Impact & Losses
The seed transaction drained 17 approved holders in one block. The collected balance diff shows attacker EOA 0x413e4fb75c300b92fec12d7c44e4c0b4faab4d04 moved from 0 to 1482323347155495672367 raw UNIBOT, a gain of 1482.323347155495672367 UNIBOT. The same artifact shows each listed victim balance dropping to zero.
The attacker also paid 16462594882250852 wei in gas during the exploit transaction. That gas spend does not change the exploit classification; the direct loss was the unauthorized depletion of approved holders' UNIBOT balances through the vulnerable router path.
7. References
- Seed exploit transaction:
0xcbe521aea28911fe9983030748028e12541e347b8b6b974d026fa5065c22f0cf - Vulnerable router:
0x126c9fbab3a2fca24edfd17322e71a5e36e91865 - Attacker helper:
0x2b326a17b5ef826fa4e17d3836364ae1f0231a6f - Real token moved: UNIBOT
0xf819d9cb1c2a819fd991781a822de3ca8607c3c9 - Evidence sources used for validation: seed transaction metadata, seed balance diff, focused exploit trace excerpt, attacker helper decompilation, router decompilation, and the verified UNIBOT source code.