56709 sc low zeroxswapverifier missing source validation
Submitted on Oct 19th 2025 at 18:55:42 UTC by @pirex for Audit Comp | Alchemix V3
Report ID: #56709
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/utils/ZeroXSwapVerifier.sol
Impacts:
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Description
Brief/Intro
The ZeroXSwapVerifier contract is supposed to validate 0x swap calldata before execution, ensuring only authorized transactions are processed. However, it fails to verify that the from address in the calldata matches the intended owner. This allows anyone controlling the calldata (relayers, aggregators, or malicious frontends) to craft payloads that drain tokens from any user who has approved the 0x router, even if that user never initiated the transaction. The result is direct theft of funds without any special privileges.
Vulnerability Details
The verifySwapCalldata() function validates that the target token matches expectations but completely ignores who the tokens are being transferred from:
function verifySwapCalldata(
bytes calldata swapCalldata,
address owner,
address targetToken,
uint256 amount
) external view returns (bool) {
// Decodes the calldata to extract transfer details
(address token, address from, address to, uint256 value) = decodeAction(swapCalldata);
// Checks token matches
require(token == targetToken, "invalid token");
// ❌ MISSING: require(from == owner, "unauthorized source");
return true;
}The function receives an owner parameter that should represent the authorized token source, but this parameter is never compared against the from address decoded from the calldata.
Here's the problem flow:
Victim approves 0x router (standard practice for any dApp using 0x swaps - via
approve()or Permit2)Attacker crafts malicious calldata with
from = victim,to = attacker,amount = 500 ETHVerifier checks token only and returns
truebecause target token matches0x executor processes the calldata and calls
transferFrom(victim, attacker, 500)Transfer succeeds because victim has existing approval
Critical note: This attack requires no admin or governance permissions. The only prerequisite is that the victim has granted an allowance (extremely common), and the attacker controls the calldata submitted to verifySwapCalldata (trivial for any relayer, aggregator, or malicious frontend).
The validator provides a false sense of security. It appears to gate the swap execution, but in reality allows arbitrary token sources as long as the token address matches.
Key insight: The owner parameter is passed in but never used in validation. This is the smoking gun - there's no point passing owner unless it's meant to be checked, but the check is simply missing.
Impact Details
This vulnerability enables:
Direct theft from approved users: Anyone who approved 0x for legitimate swaps can have their tokens stolen
No user interaction required: Victims don't need to sign any transaction or interact with the protocol
Scalable attacks: Attacker can drain all approved users systematically
Frontrunning protection bypass: Even if users think they're protected by the verifier, they're not
The financial impact is severe:
Scope: Every user who has approved 0x or Permit2 (extremely common)
Amount: Up to full approved balance per victim
Frequency: Can be executed repeatedly across all approved users
TVL at risk: Potentially millions if integrated into production
Attack requirements:
No admin or governance permissions required
Single condition: Victim must have previously granted allowance (via
approveor Permit2) to the 0x router/executor - this is extremely common for any user interacting with 0x swapsAttacker capability: Control over the calldata submitted to
verifySwapCalldata(achievable by any relayer, aggregator, or malicious frontend)No special privileges, governance, or admin access needed
For a protocol with 10,000 users who each approved $10,000 worth of tokens, total exposure is $100M. The attack is executable in a single transaction per victim.
References
Contract:
src/utils/ZeroXSwapVerifier.solCommit:
a192ab313c81ba3ab621d9ca1ee000110fbdd1e9Test:
test/ZeroXSwapVerifierBypass.t.sol
Proof of Concept
Proof of Concept
Save as test/ZeroXSwapVerifierBypass.t.sol:
Run:
Test Result:
Key observations from traces:
Verifier check passes:
ZeroXSwapVerifier::verifySwapCalldata()returnstrueeven though:The
ownerparameter is0xCAFE(passed to function)The actual
fromaddress in calldata is0xBEEF(victim)These addresses don't match, but no validation occurs
Transfer executes successfully:
transferFrom(victim: 0xBEEF, attacker: 0xA11CE, 500)succeedsVictim had previously approved the executor (standard behavior)
No signature or interaction from victim required
Funds stolen:
Victim balance: 1000 → 500 tokens
Attacker balance: 0 → 500 tokens
50% of victim's tokens stolen in single transaction
The test proves that the verifier's security guarantees are completely broken. It checks the right token but allows stealing from the wrong address.
Recommended Fix
Add explicit validation that the token source matches the authorized owner:
This single line prevents the entire attack vector by ensuring tokens can only be pulled from the address that authorized the transaction.
Additional recommendations:
Add comprehensive tests covering malicious calldata scenarios
Document security assumptions clearly in comments
Consider additional validations:
Maximum slippage bounds
Recipient address whitelist if applicable
Action selector whitelist to prevent unexpected function calls
The fix is straightforward, but the vulnerability's impact is severe. Without this check, the verifier provides zero protection against unauthorized token transfers.
Was this helpful?