56709 sc low zeroxswapverifier missing source validation

Submitted on Oct 19th 2025 at 18:55:42 UTC by @pirex for Audit Comp | Alchemix V3arrow-up-right

  • 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:

  1. Victim approves 0x router (standard practice for any dApp using 0x swaps - via approve() or Permit2)

  2. Attacker crafts malicious calldata with from = victim, to = attacker, amount = 500 ETH

  3. Verifier checks token only and returns true because target token matches

  4. 0x executor processes the calldata and calls transferFrom(victim, attacker, 500)

  5. 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 approve or Permit2) to the 0x router/executor - this is extremely common for any user interacting with 0x swaps

  • Attacker 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.sol

  • Commit: a192ab313c81ba3ab621d9ca1ee000110fbdd1e9

  • Test: test/ZeroXSwapVerifierBypass.t.sol

Proof of Concept

Proof of Concept

Save as test/ZeroXSwapVerifierBypass.t.sol:

Run:

Test Result:

Key observations from traces:

  1. Verifier check passes: ZeroXSwapVerifier::verifySwapCalldata() returns true even though:

    • The owner parameter is 0xCAFE (passed to function)

    • The actual from address in calldata is 0xBEEF (victim)

    • These addresses don't match, but no validation occurs

  2. Transfer executes successfully:

    • transferFrom(victim: 0xBEEF, attacker: 0xA11CE, 500) succeeds

    • Victim had previously approved the executor (standard behavior)

    • No signature or interaction from victim required

  3. 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.


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:

  1. Add comprehensive tests covering malicious calldata scenarios

  2. Document security assumptions clearly in comments

  3. 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?