TL;DR
On January 25, 2026, an attacker exploited an arbitrary call vulnerability in SwapNet's aggregator contracts to drain $13.4 million from a single user on Base. The exploit weaponized persistent ERC-20 token approvals — turning a convenience feature into a loaded gun. This article dissects the vulnerability, explains why infinite approvals remain DeFi's most underestimated attack surface, and provides a defense playbook for both developers and users.
The Attack: 45 Minutes to $13.4 Million
SwapNet operated as a DEX aggregator, routing trades through optimal liquidity paths. Its contracts needed to move tokens on behalf of users, which meant users had to approve SwapNet's contracts to spend their tokens.
The critical flaw was an arbitrary external call vulnerability — insufficient input validation in SwapNet's routing logic allowed an attacker to:
- Craft malicious call parameters that bypassed the intended routing logic
-
Hijack the contract's low-level
call()function to invoke arbitrary targets -
Call
transferFrom()on ERC-20 contracts using the SwapNet contract asmsg.sender - Drain tokens from any user who had granted SwapNet a persistent allowance
The attacker walked away with approximately 10.5 million USDC, swapped it for ~3,655 ETH on Base, and began bridging to Ethereum mainnet. SwapNet paused contracts 45 minutes later — but the damage was done.
Why Only 20 Users Were Affected
Here's the twist: Matcha Meta (the DEX frontend using SwapNet) had a "One-Time Approval" feature enabled by default. This routed approvals through 0x's AllowanceHolder contract, which automatically revoked allowances after each trade.
The 20 victims had manually disabled this safety feature, granting direct, persistent (often infinite) allowances to SwapNet's contracts. One user alone lost $13.34 million.
Anatomy of an Arbitrary Call Vulnerability
Arbitrary call bugs are among the most dangerous smart contract vulnerabilities because they let an attacker impersonate the vulnerable contract. Here's the pattern:
// DANGEROUS: Arbitrary call with user-controlled target and data
function executeRoute(
address target,
bytes calldata data,
uint256 value
) external {
// Missing: validation that 'target' is a whitelisted DEX
// Missing: validation that 'data' doesn't call transferFrom/approve
(bool success, ) = target.call{value: value}(data);
require(success, "Route execution failed");
}
When a contract holds token approvals from users, an arbitrary call effectively gives the attacker proxy access to every approved token balance.
The Kill Chain
1. Identify contract with arbitrary call vulnerability
2. Find users who granted persistent allowances to that contract
3. Craft calldata: target = USDC contract, data = transferFrom(victim, attacker, balance)
4. Execute through the vulnerable contract (which has the allowance)
5. Contract's address is msg.sender → transferFrom succeeds
6. Repeat for every approved user/token pair
The Infinite Allowance Problem
The SwapNet exploit is a symptom of a deeper disease. Consider these numbers:
- Aperture Finance lost $3.7M in the same week to the same vulnerability class
- An estimated $2.7 billion in token approvals are outstanding to unaudited contracts on Ethereum alone
- The average DeFi user has 47 active approvals across protocols they no longer use
Why Infinite Approvals Exist
Every ERC-20 interaction requires an approve() transaction before the actual operation. To save users gas fees and friction, protocols request type(uint256).max approval — effectively saying "you can spend all my tokens, forever."
// The standard "infinite approval" pattern
token.approve(spenderContract, type(uint256).max);
This made sense when gas was $50 per transaction. It makes less sense when:
- The approved contract has an unverified, closed-source implementation
- The approval persists after you stop using the protocol
- A single vulnerability in the approved contract compromises every approver
Historical Toll
| Year | Exploit | Root Cause | Loss |
|---|---|---|---|
| 2023 | Multichain | Compromised keys + persistent approvals | $126M |
| 2024 | Socket Gateway | Arbitrary call + infinite approvals | $3.3M |
| 2025 | 1inch Resolver | Arbitrary call + stale approvals | $5M |
| 2026 | SwapNet | Arbitrary call + disabled safety features | $13.4M |
The pattern is clear: persistent approvals transform any contract vulnerability into a user fund vulnerability.
Defense Playbook
For Smart Contract Developers
1. Whitelist call targets
mapping(address => bool) public whitelistedTargets;
function executeRoute(
address target,
bytes calldata data,
uint256 value
) external {
require(whitelistedTargets[target], "Target not whitelisted");
// Additional: block calls to ERC-20 approve/transferFrom selectors
bytes4 selector = bytes4(data[:4]);
require(
selector != IERC20.approve.selector &&
selector != IERC20.transferFrom.selector &&
selector != IERC20.transfer.selector,
"Forbidden selector"
);
(bool success, ) = target.call{value: value}(data);
require(success);
}
2. Implement per-transaction allowances
Follow the Permit2 or AllowanceHolder pattern:
// User approves Permit2 once
// Each protocol interaction uses a signed permit with:
// - Specific amount
// - Specific spender
// - Expiration timestamp
// - Single-use nonce
3. Never hold persistent approvals in routing contracts
Aggregator contracts should use pull-based patterns where tokens are transferred to an intermediary, processed, and the intermediary is emptied in the same transaction.
For DeFi Users
1. Audit your approvals regularly
- Use Revoke.cash or Etherscan Token Approval Checker
- Revoke approvals for protocols you no longer use
- Set calendar reminders for quarterly approval audits
2. Prefer limited approvals
- Approve only the exact amount needed for each transaction
- Use wallets that default to exact-amount approvals (Rabby, for example)
3. Never disable safety features
- If a frontend offers one-time approvals, keep them on
- The gas savings from infinite approval are not worth the tail risk
4. Separate hot wallets
- Never keep $13M in a wallet with active DeFi approvals
- Use a dedicated trading wallet with limited funds
- Transfer profits to a cold wallet with zero approvals
The Bigger Picture: ERC-20 Approvals Need a Rethink
The approval model was designed in 2015 for a simpler DeFi landscape. Today's composable, multi-protocol interactions create approval chains where:
- User approves Aggregator A
- Aggregator A approves Router B
- Router B approves Pool C
- A vulnerability anywhere in this chain can drain user funds
EIP-2612 (Permit) and Uniswap's Permit2 are steps in the right direction, enabling gasless, expiring, single-use approvals. But adoption is uneven — most protocols still default to type(uint256).max.
The SwapNet exploit should be a wake-up call: every infinite approval is a contingent liability. The question isn't whether the approved contract will be exploited — it's when.
Key Takeaways
- Arbitrary call vulnerabilities + persistent approvals = catastrophic loss — SwapNet proved this again
- One-time approvals saved the majority of users — Matcha Meta's default saved all but 20 users
- Audit your approvals today — if you've been in DeFi for more than a year, you probably have dozens of stale approvals
- Developers: whitelist targets AND block sensitive selectors — either alone is insufficient
- The $13M single-user loss was entirely preventable — wallet hygiene and approval management would have eliminated the risk
This article is part of the DeFi Security Deep Dives series. Follow for weekly analysis of real-world exploits and actionable security guidance.
Disclaimer: This analysis is for educational purposes only. The author has no affiliation with any mentioned protocols.
Top comments (0)