Unizen Trade Aggregator v1 Unverified Call-Array Arbitrary Call / Approval Drain (2024-03-08)

  • Name: Unizen Trade Aggregator v1 unverified call-array arbitrary call / approval drain
  • Date: 2024-03-08
  • Protocol: Unizen
  • Chain: Ethereum
  • Category: router approval abuse / unverified external call / token-approval drain / upgrade regression
  • Loss: about $2.1 million across multiple wallets
  • Summary: Shortly after Unizen upgraded its Ethereum trade-aggregator proxy, attackers discovered that the old v1 Trade Aggregator at 0xd3f64BAa732061F8B3626ee44bab354f854877AC would accept an attacker-shaped call array and forward it into an unverified external-call path. Because users had already approved that aggregator as spender, the attacker did not need to steal funds from protocol custody. They only needed to make the aggregator itself call token contracts with calldata like transferFrom(victim, aggregator, amount), then finish the route so the pulled tokens were forwarded to the attacker. Unizen’s own docs had just promoted a single address approval model and a gas-optimization push around the same router surface, so one broadened approval object plus one under-validated execution path turned old wallet allowances into a direct drain primitive.
  • Trigger / entry point:
    • Unizen’s Ethereum v1 Trade Aggregator proxy at 0xd3f64BAa732061F8B3626ee44bab354f854877AC was still widely approved by users.
    • Unizen’s documentation later described the newer architecture as a unified approval / executor model with a single address approval surface, which is the same operational shape that made lingering approvals dangerous on the old router as well.
    • An upgrade transaction cited in public incident reporting (0x858c71876a5ad43ffe27f227ad01979f3bd5de95e66194e98cea41f53a43ef59) landed shortly before the exploit window.
    • Public exploit reproductions show attacker-controlled calldata hitting selector 0x1ef29a02 on the aggregator, together with a caller-supplied Call[] object whose elements included arbitrary target, amount, and raw data bytes.
    • That execution path let the router speak to token contracts with attacker-chosen calldata even though the route was supposed to represent a legitimate swap plan.
  • Root cause:
    • Unizen combined broad ERC-20 spender authority from old router approvals with an execution path that trusted a caller-supplied external-call bundle too much.
    • The intended invariant was effectively the trade aggregator may only execute the exact swap / bridge steps that correspond to a canonical quoted route; the deployed path instead let attacker-controlled Call.target and Call.data redefine what economic action the router would perform after route selection.
    • Exploit reproductions show the attacker did not need a fake DEX or a compromised operator key. The aggregator itself became the approved-spender execution primitive once it would forward a call to a token contract.
    • The sharper authority miss was not merely one function missed validation, but no route-local object kept approved spender identity, victim payer scope, token target, selector family, and quoted trade meaning fused above raw allowance.
    • In other words, users approved Unizen, the router is executing a trade, and this exact downstream token call is part of that trade were treated as if they were the same fact. They were not.
  • Exploit path:
    1. The attacker identified that the Ethereum v1 Trade Aggregator proxy still held large historical allowances from user wallets.
    2. They called the unverified router path exposed through selector 0x1ef29a02, passing a crafted Info object and a Call[] array.
    3. Inside that call array, the attacker set target to an ERC-20 token contract such as VRA (0xF411903cbC70a74d22900a5DE66A2dda66507255) or DMTR (0x51cB253744189f11241becb29BeDd3F1b5384fdB).
    4. The attacker encoded raw token calldata like transferFrom(victim, TradeAggregator, amount) inside Call.data.
    5. Because the token contract saw the already-approved Trade Aggregator as caller, it honored the transfer and moved the victim’s approved balance into the aggregator.
    6. The route logic then completed with attacker-controlled recipient semantics so the value ended up in attacker-controlled wallets rather than in a real swap settlement path.
    7. Multiple attackers repeated the pattern over a period of hours against approved wallets and different tokens until revocations and mitigation slowed the drain.
  • What the attacker needed to be true:
    • Victim wallets needed existing approvals to the Unizen Trade Aggregator large enough to make theft worthwhile.
    • The 0x1ef29a02 router path had to keep accepting attacker-shaped Call[] data without proving the target and data fields still matched a legitimate route.
    • The aggregator had to preserve its spender identity while performing those low-level calls so token contracts would accept transferFrom().
    • The attacker needed quick wallet discovery because the exploit monetized lingering approvals rather than protocol-held TVL.
  • Technical anatomy worth remembering:
    • This was an approval drain, not a treasury drain. The critical balance sheet sat in user wallets’ allowance tables.
    • Public exploit reproductions are unusually clean here: one variant pulled DMTR from victim 0x7feAeE6094B8B630de3F7202d04C33f3BDC3828a, another pulled VRA from victim 0x12fe4bC7D0B969055F763C5587F2ED0cA1b334f3, and both did it by making the router call the token contract itself.
    • The vulnerable surface was an unverified or under-decoded external-call bundle, not a subtle price-oracle bug. The attack path was approved router + attacker-defined token target + attacker-defined calldata.
    • Unizen’s later documentation is analytically useful because it makes the approval blast radius explicit: users were encouraged to rely on a unified approval / executor surface. That kind of UX streamlining is exactly why every route-local execution hop has to stay narrower than the raw spender privilege.
    • This incident sits in the same exploit family as Socket, LI.FI, Transit Swap, and Seneca, but its flavor is especially stark: the attacker did not need a fancy integration bypass; they only needed the approved router to accept one attacker-authored external call object.
    • The clean positive-design companion is approval-funded route authority truth as a single security object: Unizen lacked a narrower object proving that the approved spender, the quoted route, the downstream target family, and the final recipient meaning all still belonged to one canonical trade story.
  • Representative transactions and addresses:
    • Vulnerable Ethereum Trade Aggregator proxy: 0xd3f64BAa732061F8B3626ee44bab354f854877AC
    • Upgrade transaction preceding the exploit window: 0x858c71876a5ad43ffe27f227ad01979f3bd5de95e66194e98cea41f53a43ef59
    • Example exploit transaction highlighted by DeFiHackLabs: 0x923d1d63a1165ebd3521516f6d22d015f2e1b4b22d5dc954152b6c089c765fcd
    • Another exploit transaction highlighted by DeFiHackLabs: 0xdd0636e2598f4d7b74f364fedb38f334365fd956747a04a6dd597444af0bc1c0
    • Attacker address used in one public reproduction: 0x2aD8aed847e8d4D3da52AaBB7d0f5c25729D10df
    • Publicly reported attacker cluster: 0xb660cae1a59336676ea1887b15eb3c0badb90d78
    • Example affected token: DMTR 0x51cB253744189f11241becb29BeDd3F1b5384fdB
    • Example affected token: VRA 0xF411903cbC70a74d22900a5DE66A2dda66507255
  • Response / mitigation:
    • Unizen acknowledged the incident publicly hours after the first drain transactions.
    • Public reporting and community tooling focused on immediate approval revocation because the danger sat in lingering spender permissions.
    • The team later said it patched the affected gas-optimization / aggregator surface and began reimbursing smaller losses from treasury / founder-provided funds.
    • The enduring mitigation lesson is not just patch faster; it is never let an approval-funded router accept a generic external-call object unless target family, selector family, payer scope, and recipient semantics are all already pinned by route-local policy.

Pattern tags

  • arbitrary-call-token-approval-drain
  • Upgrade-era router changes widened an already dangerous approval surface: users had historical approvals to the Unizen Trade Aggregator, and a recently changed execution path let attacker-authored Call[] data survive far enough to become raw token calls
  • The router’s own external-call object became the theft primitive: instead of routing through a canonical quoted swap, the attacker pointed Call.target at token contracts and supplied transferFrom(...) calldata directly
  • Existing wallet approvals became live theft authority: once the aggregator would speak attacker-defined token calldata, the protocol’s legitimate spender identity was enough to drain wallets without touching escrowed TVL
  • Quoted trade meaning and downstream call meaning came apart: the router still looked like a trade executor at the surface, but nothing kept approved spender, this route, and this exact token call fused as one security object

Why the exploit worked

  • Unizen’s security story implicitly depended on one rule: if the approved trade aggregator is executing a route, every downstream external call still has to be provably part of that route. The 0x1ef29a02 path broke that rule by letting caller-shaped Call[] data choose token targets and raw calldata after the router boundary.
  • The attacker did not need governance, key theft, or price manipulation. They only needed an already-approved router to treat an attacker-authored external call bundle as if it were a valid trade step.
  • The exploit is more instructive than the short label unverified external call because the real failure was route-authority collapse. User approved the router, router is executing a trade, and token transfer belongs to that trade collapsed into one unchecked assumption.
  • Unizen’s single-address approval ergonomics made the blast radius operationally larger, not because those ergonomics were inherently wrong, but because they sharpen the need for downstream execution to stay much narrower than the standing spender privilege.
  • The healthier design would have required a route-local authorization object that pinned token-in, amount, approved spender surface, allowed counterparties, allowed selectors, and recipient semantics before any external call left the router.

Detection and audit pivots

  • Grep every aggregator entrypoint that accepts externally supplied arrays of target, data, executor, router, or step objects. The key question is whether any of those paths can cause the approved router to call an ERC-20 token contract directly.
  • Treat unverified or weakly decoded route blobs as first-class approval-drain risk, not just as generic integration risk. If users have already approved the contract, a malformed route object is a wallet-drain primitive.
  • Diff upgrade-era router changes specifically for new helper path, gas optimization, or unified approval work, because those changes often widen the most dangerous contract in the system: the already-approved spender.
  • Add regression tests for the negative property that no public trade path can make the router execute transferFrom(victim, attacker, amount) or any equivalent token-pull flow against arbitrary victims even when the router already has allowance.
  • Maintain fast incident-response inventory for spender addresses users approve at scale; when an approval-backed router fails, operators need to know the revoke list immediately.

Failure-prevention checklist

  • Treat every approval-surface simplification as a security-boundary expansion. If documentation or product changes encourage users to consolidate around one single address router, require stronger route-local proof about which targets, selectors, recipients, and payer scopes that router may reach on every path.
  • Bind opaque Call[] execution objects to typed route semantics before execution. A trade step should carry an allowlisted counterparty family, selector family, token-in / token-out meaning, and recipient semantics instead of trusting raw target plus data bytes to remain honest.
  • Keep the router from calling token contracts directly except on narrowly enumerated, separately reviewed helper paths. For swap aggregators, token contract is the low-level target should be an explicit deny-by-default rule.
  • Require upgrade review to ask the spender-centric question first: what can this already-approved router do now that it could not do before? Gas-optimization and migration work should inherit the same target / selector constraints as the original route path.
  • Separate route selection from route execution authority. User chose Unizen, router selected a route, and this exact external call belongs to that route should be proved as three linked facts rather than inferred from one another.
  • Inventory legacy spender addresses and publish revoke-ready guidance before incidents. Unizen’s own contract-address docs preserved the exact v1 aggregator address, which is useful operationally; teams should treat that inventory as part of incident readiness, not just integrator documentation.
  • Pair any broad approval surface with bounded-lifetime permits, exact-amount spending, or staged route-local consumption where feasible so stale allowances do not remain reusable fuel for unrelated execution branches.
  • Add negative tests for direct token-call variants. If attacker-shaped route data can ever make the router encode transferFrom(victim, router, amount), approve(attacker, amount), or a recipient-changing token verb, the route should fail before balances move.

Audit prompts

  • Which public entrypoints can decode a caller-supplied array of target, amount, and raw calldata while the contract is still acting as an approved spender for user wallets?
  • After route selection, what trusted local state proves that each external call still belongs to the quoted swap story rather than to a generic token-pull story?
  • Can any route step point directly at an ERC-20 token contract, and if yes, what exact selector family is allowed there?
  • Do migration, gas-optimization, or unified-approval changes inherit the same target / selector validation path as older router logic, or do they introduce a second, weaker execution lane?
  • Where does the system prove payer scope? If the router can already spend from a victim wallet via allowance, what prevents attacker-authored calldata from redefining who is funding the step and who ultimately receives the proceeds?
  • Are there regression tests asserting that no public router path can execute transferFrom(victim, router, amount) or equivalent token-pull behavior against arbitrary approved wallets?
  • If one spender address fails, how quickly can operators enumerate all supported chains, successor router addresses, and currently recommended revoke targets from one authoritative contract inventory?
  • What exact object keeps approved spender identity, this route, this selector family, and this recipient meaning fused above raw allowance once route bytes are decoded?

Lessons that should already be standard

  • Approval simplification is not free UX; it is concentrated spend authority. The more a protocol trains users to approve one router everywhere, the more every route-decoding bug becomes a wallet-wide incident.
  • Opaque route objects are custody-critical whenever the router already holds allowances. Call[] is not mere integration plumbing if those bytes can tell an approved spender which token contract to talk to next.
  • The router is executing a trade is not a meaningful security proof. The protocol still has to prove which exact downstream counterparty and selector family count as part of that trade.
  • Upgrade-era gas optimization work deserves the same threat model as a new custody primitive. Small execution-lane changes on an already-approved router can silently create a mass wallet drainer.
  • Legacy approvals are part of live blast radius even after migration guidance changes. Moving users toward Permit2 or a newer router helps only if old spender surfaces are deprecated, inventoried, and easy to revoke.
  • Contract-address documentation is security tooling as much as integrator tooling. During approval drains, a clean authoritative list of spender addresses materially shortens user revocation time.
  • The healthy design target is not router validates calldata better; it is router never gains more reachable execution authority than the narrow route meaning users thought they were approving.

Why this incident matters

  • It is a clean example of a DEX aggregator draining wallets without ever taking custody of user funds.
  • It shows how route objects and external-call bundles become part of the approval object the moment one approved router is allowed to interpret them.
  • It also reinforces the operational lesson that gas optimization and approval simplification changes deserve spender-surface review, not just product review.
  • The preserved design lesson is sharper than validate calldata: once a router holds broad allowances, every downstream execution hop has to keep spender identity narrower than reachable call authority.

Internal linkages

Control surface