Rubic Arbitrary Call (2022)
Smart contract security audit by TierZero · 2023-08-15
Independent research. This is an unsolicited security analysis published by TierZero — not a commissioned audit, and no certificate is issued. It reflects our own review of public code and on-chain events.
Findings summary
Independent research — not a commissioned audit. This report is based on public post-incident disclosures and blockchain data; it does not claim to identify any new or undisclosed vulnerability in live software.
What the protocol did
Rubic operated a cross-chain swap aggregator: a router contract that let users approve a single spender and then route a trade through whichever DEX or bridge offered the best execution, on Ethereum and several other EVM chains. The standard flow was the standard aggregator flow everywhere — approve(router, amount) once, then call a swap-style entry point and let the router pick and call the underlying exchange contract on the user's behalf. In September 2022, an attacker found that this entry point would forward attacker-supplied calldata to an attacker-supplied address, and used it to pull tokens out of wallets that had approved the router for entirely unrelated, legitimate swaps. Public reporting put the loss in the range of roughly $1.4 million across several tokens.
The vulnerability
The bug class is "unvalidated arbitrary external call," and it's a repeat offender in aggregator and router contracts — the same shape shows up in Furucombo (2021) and later in Li.Fi (2024). The router exposed a function that accepted a target address and raw calldata, then executed target.call(data) without checking that target was a known, trusted DEX/aggregator contract, and without checking that data decoded to an expected swap-related selector.
Because the router itself held ERC-20 allowances from every user who had ever approved it — often unlimited allowances, since that's the UX default most aggregators shipped — any call the router made was made as the router. An attacker didn't need to touch a victim's wallet at all. They just needed the router to execute a call where msg.sender was the router and the call target was the token contract itself.
Flawed pattern, simplified to the essential mistake:
// Router already holds approvals from many past swap users.
function executeSwap(address target, bytes calldata data) external {
(bool ok, ) = target.call(data);
require(ok, "swap failed");
// no check that `target` is a whitelisted DEX/aggregator
// no check that `data`'s selector is swap-related
}
Nothing here validates target. An attacker can set target = <any ERC20 the router has allowances on> and data = abi.encodeWithSelector(IERC20.transferFrom.selector, victim, attacker, amount). The router's own privileged allowance does the rest.
How the exploit worked
- The attacker scanned on-chain approval events to find ERC-20 tokens where holders had granted allowance to the Rubic router — the normal residue of past, legitimate swaps.
- For each token/victim pair, the attacker crafted calldata for
transferFrom(victim, attacker, amount). - The attacker called the router's arbitrary-call swap function, passing the token contract as
targetand the craftedtransferFromcalldata asdata. No swap logic, price check, or slippage bound was ever actually exercised — the "swap" call was a disguise. - The router executed
target.call(data). Since the router wasmsg.senderand held the necessary allowance, the ERC-20 contract accepted the transfer as legitimate — it has no way to know the call originated from a forged instruction rather than a real trade. - The attacker repeated this across multiple victims and tokens, then moved the proceeds off-chain through mixers/bridges before the team could pause the contract.
Nothing in this sequence required a reentrancy bug, an oracle manipulation, or a signature flaw — it was a single missing allowlist check on a function that already had privileged spending power over thousands of wallets.
How an audit catches this
This is one of the highest-value checks in a router/aggregator review, and it's mechanical enough to make a checklist item rather than a judgment call:
- Grep every low-level call site. Any
address.call(data),delegatecall, or dynamic-dispatch pattern wheretargetordata(or both) originate from calldata is a flag until proven otherwise. - Require a target allowlist. The contract should hold a mapping of trusted DEX/router/bridge addresses set by governance or a timelocked admin, and revert on anything else.
- Constrain the calldata shape. Even against a whitelisted target, validate the function selector matches an expected swap interface — don't let a whitelisted DEX's
transferFrom-adjacent functions become an attack surface too. - Fuzz the entry point with adversarial targets. A concrete regression test: call the swap function with
targetset to a token the router holds an approval for, and calldata encodingtransferFrom; assert it reverts. - Check balance deltas, not just call success. The contract should verify pre/post balances match the intended trade for the intended token pair, independent of whether the inner call "succeeded."
- Treat unlimited approvals as a standing liability, not a UX convenience — the router's blast radius is bounded by the sum of every allowance it has ever been granted, which grows unboundedly over the contract's life.
We run this exact class of check as part of every smart contract audit we perform on router, aggregator, or proxy-swap contracts, and it's also standard scope in a code review pass over any contract that forwards user-controlled calldata. The general principle — never let an external call's target and payload both be attacker-controlled without a whitelist — carries over directly from the cross-program invocation risk we flag in Solana DeFi programs, which is the same "who am I actually calling, and did I check" problem in a different execution model.
Remediation
The fix Rubic and similar protocols converged on afterward is a combination of allowlisting and reduced allowance exposure:
- Allowlist call targets to a fixed, governance-controlled set of known DEX/aggregator contracts; reject anything else at the entry point.
- Selector-check the calldata against the expected interface for the whitelisted target, not just "did the call not revert."
- Move off unlimited
approve()toward scoped, expiring allowances — the pattern we cover in our review of Permit2-style gasless approvals and their own sniping-bot exposure — so a compromised router can only ever move what a user actually intended for the current trade, not everything they've ever approved. - Add a circuit breaker. Anomalous outflow patterns (many victims, one destination, in a short window) should be detectable and pausable before losses compound.
- Independently verify balance deltas on both legs of the swap before the transaction is allowed to complete, so a forged call that doesn't actually perform a swap has no path to success even if it passes the target/selector checks.
If your protocol routes calldata on behalf of users who've granted it standing allowances, this is exactly the class of finding we look for first — talk to us about an audit.
Need your contracts audited?
Manual review + tooling across TON, Solana and EVM — certificate and full report included.
Get an audit