57749 sc low zeroxswapverifier misses critical sender recipient minout validations allowing malicious 0x calldata to drain funds critical direct theft

Submitted on Oct 28th 2025 at 16:49:01 UTC by @humaira45 for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #57749

  • 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 library is intended to validate 0x/Matcha calldata before forwarding it to an executor (i.e., 0x Settler/Router). However, the current implementation does not enforce critical invariants:

  • It does not validate SlippageAndActions.recipient, buyToken, or minAmountOut at the top level.

  • For several actions, notably TRANSFER_FROM, it only checks token == targetToken and ignores:

    • The source address (from) is the whitelisted owner,

    • The destination address (to) is a safe recipient,

    • Any amount or minAmountOut limiting bound.

As a result, an attacker can craft 0x calldata that passes verification and, once forwarded to the executor, transfers tokens directly from the approved source to the attacker. This enables direct theft of funds when used as intended (verify → forward).

Vulnerability Details

What the contract does (intended)

  • decodeAndVerifyActions and verifySwapCalldata parse 0x Settler calldata (execute / executeMetaTxn) and are expected to validate:

    • Actions types are allowed and within bounds,

    • Sender/recipient semantics (from is whitelisted source; destination is safe),

    • buyToken/minAmountOut are sane and match the intended swap outcome.

Where verification fails

  • Top-level fields are never validated:

    • SlippageAndActions.recipient is ignored.

    • SlippageAndActions.buyToken is ignored.

    • SlippageAndActions.minAmountOut is ignored.

  • TRANSFER_FROM action (_verifyTransferFrom) only checks token == targetToken, but does not verify:

    • from == owner (the whitelisted source address provided to the verifier),

    • to == recipient or any safe recipient,

    • amount bounds/minOut.

  • Other actions are also too permissive:

    • _verifySellToLiquidityProvider: only checks sellToken == targetToken.

    • _verifyRFQVIP: ignores amount entirely.

    • _verifyUniswapV3VIP: ignores recipient/buyToken/minAmountOut.

Root cause (code level)

Impact Details

Severity: Critical — Direct theft of funds

  • An attacker can craft a 0x payload containing a TRANSFER_FROM action that pulls tokens from a source address (with allowance) directly to the attacker’s address. ZeroXSwapVerifier “approves” this payload, and once the calldata is forwarded to the 0x executor, the transfer occurs.

  • This maps to “Direct theft of funds” in the program taxonomy.

References

  • In-scope file: src/utils/ZeroXSwapVerifier.sol

    • Functions: verifySwapCalldata, _verifyExecuteCalldata, _verifyExecuteMetaTxn, _verifyAction, _verifyTransferFrom

  • Test reference (their tests): src/test/ZeroXSwapVerifier.t.sol shows TRANSFER_FROM verification returns true without binding from/to.

https://gist.github.com/humairar301-droid/0f349aa30540edb06c7a620a2ab0de83

Proof of Concept

Proof of Concept

What this PoC proves (end-to-end)

  • A production-like harness ZeroXSwapExecutorHarness verifies the payload via ZeroXSwapVerifier and then forwards the same calldata to a simulated 0x executor (FakeSettlerSimulator).

  • The payload includes a TRANSFER_FROM action that moves tokens from a funded owner to an attacker.

  • The verifier returns true (payload “valid”), and the executor successfully transfers tokens to the attacker.

  • We also prove:

    • The verifier still returns true when action.from != owner (mismatch ignored),

    • The meta-transaction variant (executeMetaTxn) is equally vulnerable.

PoC files (add to repo)

  • This Gist contains the PoC: https://gist.github.com/humairar301-droid/0f349aa30540edb06c7a620a2ab0de83

  • src/utils/ZeroXSwapExecutorHarness.sol

  • Minimal executor that verifies and forwards the calldata to an external “settler.”

  • Bubbles up revert reasons if the settler reverts.

  • src/test/ZeroXSwapVerifier_ExecutorDrain.t.sol

  • End-to-end tests:

    • test_ZeroXVerifier_EndToEnd_DrainViaExecutor (execute selector, recovers funds directly to attacker) test_ZeroXVerifier_VerifyAllowsMismatchedFromAndRecipient_StillDrains (from != owner still verifies and drains) test_ZeroXVerifier_EndToEnd_DrainViaMetaTxn (executeMetaTxn selector, also drains)

How to run

  • Commands:

    • forge test --match-test test_ZeroXVerifier_EndToEnd_DrainViaExecutor -vvvv --evm-version cancun

    • forge test --match-test test_ZeroXVerifier_VerifyAllowsMismatchedFromAndRecipient_StillDrains -vvvv --evm-version cancun

    • forge test --match-test test_ZeroXVerifier_EndToEnd_DrainViaMetaTxn -vvvv --evm-version cancun

Representative results (from your traces)

Why this is in-scope and feasible

  • ZeroXSwapVerifier is explicitly in-scope, and Alchemix requested dedicated attention to ensure in-place verification matches 0x logic (sender/recipient/amount/slippage).

  • The exploit does not rely on edge conditions or oracle manipulation; it is purely a verification-logic gap.

  • The repository already follows a Permit2-based approval pattern in strategies (MYTStrategy manages Permit2 and sets approvals). It is realistic that, after verification, an executor/settler has the ability to pull tokens per the payload.

Suggested remediation

Fail-closed design and minimal checks

  • Top-level (execute/executeMetaTxn):

    • require(saa.recipient == expectedRecipient) — bind to a safe recipient (e.g., the calling strategy or a known sink).

    • require(saa.buyToken == targetToken) — ensure intended output asset is enforced.

    • require(saa.minAmountOut >= configuredBound) — enforce slippage and minimum output.

    • Thread through allowedRecipient to all action verifiers.

  • TRANSFER_FROM:

    • require(from == owner) — enforce the only whitelisted source is used.

    • require(to == allowedRecipient) — ensure funds cannot be directed to arbitrary addresses.

    • Optionally enforce amount bounds (or validate against minAmountOut).

  • Other actions (RFQ VIP, SellToLiquidityProvider, UniswapV3 VIP, Velodrome V2 VIP):

    • Validate sender/recipient/token path and minAmountOut/bps as appropriate for each action’s ABI.

    • Reject (fail-closed) action types you cannot fully parse/validate now.

Optional hardening

Was this helpful?