57514 sc low calldata verification bypass in 0x preflight logic enables arbitrary from recipient manipulation and direct fund theft

Submitted on Oct 26th 2025 at 21:23:55 UTC by @dizaye for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #57514

  • 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 in-repo 0x pre-execution validation library fails to enforce critical invariants (sender/from, recipient, buyToken, minAmountOut). A malicious route that passes this verifier can still direct transfers from an unintended address (victim) to an attacker or route the final output to an attacker-selected recipient. If wired as a guard before calling the 0x Settler (as intended per scope), this results in direct theft of funds.

Vulnerability Details

Top-level verification accepts only specific Settler selectors but does not bind or enforce destination semantics: decodeAndVerifyActions: ZeroXSwapVerifier.decodeAndVerifyActions() branches on the first 4 bytes (execute / executeMetaTxn), then delegates to sub-verifiers without constraining the “from” party or global outputs.

No check that action “from” equals the expected owner:

  • TRANSFER_FROM verifier decodes (token, from, to, amount) but never asserts from == owner. A forged action with from = victim and owner = attacker passes verification:

    • ZeroXSwapVerifier._verifyTransferFrom()

  • Consequence: an attacker can pass verification even when the underlying transfer would pull funds from a third party (victim).

  • buyToken and minAmountOut decoded but never enforced:

    • execute path: ZeroXSwapVerifier._verifyExecuteCalldata()

    • meta-txn path: ZeroXSwapVerifier._verifyExecuteMetaTxnCalldata()

    • Fields SlippageAndActions.buyToken and SlippageAndActions.minAmountOut are ignored, allowing unexpected output assets and/or negligible output.

  • Recipient not enforcedd:

    • SlippageAndActions.recipient is never validated against an expected recipient, enabling malicious redirection to attacker-controlled addresses.

  • Per-action checks are insufficient:

    • Basic Sell to Pool: token and bps only; no recipient/minOut/“from” invariant: ZeroXSwapVerifier._verifyBasicSellToPool()

    • UniswapV3 VIP: relies on simplified fill parsing; ignores recipient/minOut invariants: ZeroXSwapVerifier._verifyUniswapV3VIP()

    • RFQ VIP: ignores minOut and recipient; “from” is unchecked: ZeroXSwapVerifier._verifyRFQVIP()

    • Sell to Liquidity Provider: token and some amounts only; ignores recipient/minOut and “from” == owner: ZeroXSwapVerifier._verifySellToLiquidityProvider()

    • Velodrome V2 VIP: token and bps only; ignores recipient/minOut and “from” == owner: ZeroXSwapVerifier._verifyVelodromeV2VIP()

  • The library currently focuses on allowing or denying action selectors and very high-level parameters (sellToken, bps) but omits hard binding of:

    • “Who is being charged?” (from == owner)

    • “Who ultimately receives?” (recipient)

    • “What token and how much must arrive?” (buyToken, minAmountOut)

Threat model fit

  • Program brief explicitly calls out the need for strict 0x calldata verification to prevent manipulation of tokens, senders, amounts, receivers, slippages. The current implementation does not meet that requirement.

Impact Details

  • Direct loss of funds:

    • If the verifier is used to authorize a subsequent call to the 0x “Settler,” attackers can pass verification with a route that:

      • Pulls tokens from a victim (from != owner) and credits attacker.

      • Routes final output to attacker recipient ignoring SlippageAndActions.recipient.

      • Bypasses minAmountOut, capturing value through slippage or dust output.

  • Blast radius:

    • Any vault/strategy or adapter that relies on this library’s “verified” output before execution is exposed.

    • With Permit2 or allowance in play, the route can transfer on behalf of a victim or the contract itself if approvals exist.

  • Economic viability:

    • The Integration PoC shows tokens are moved from victim ->> attacker after the real verifier returns true. With real DEX paths, the same proof-of-possibility extends to routing outcomes against the intended invariant.

Proof of Concept

Proof of Concept

A) Library-only PoC (real file import)

PoC File: src/test/ZeroXSwapVerifier_PoC.t.sol

Run: forge test --match-path src/test/ZeroXSwapVerifier_PoC.t.sol -vvvv --evm-version cancun

Key tests (all PASS)

  • TRANSFER_FROM from != owner:

    • ZeroXSwapVerifier_PoC.test_PoC_TransferFrom_FromNotEqualOwner_PassesVerification()

  • Recipient ignored:

    • ZeroXSwapVerifier_PoC.test_PoC_RecipientIgnored_PassesVerification()

  • buyToken/minAmountOut ignored:

    • ZeroXSwapVerifier_PoC.test_PoC_BuyTokenAndMinAmountOut_Ignored()

Outputt (abridged)

  • verifySwapCalldata returns true in all malicious configurations:

    • … ZeroXSwapVerifier::verifySwapCalldata(…, owner=0x…A11cE, targetToken=TestERC20[…], maxSlippage=1000) → Return true

B) Integration PoC (verification + execution “drain”)

Poc File: src/test/ZeroXSwapVerifier_IntegrationPoC.t

Files

  • Harness: verify via real library then execute first action on a minimal downstream “settler” to model production flow.

    • VerifierExecHarness.verifyThenExecute()

  • Minimal executor:

    • MockSettler.transferFrom()

  • Test:

    • ZeroXSwapVerifier_IntegrationPoC.test_PoC_Integration_DrainsVictimAfterVerification()

Run: forge test --match-path src/test/ZeroXSwapVerifier_IntegrationPoC.t.sol -vvvv --evm-version cancun

Output (abridged, with real library) [PASS] test_PoC_Integration_DrainsVictimAfterVerification() Traces: ZeroXSwapVerifier_IntegrationPoC::test_PoC_Integration_DrainsVictimAfterVerification() VerifierExecHarness::verifyThenExecute(...) ZeroXSwapVerifier::verifySwapCalldata(...) → Return true MockSettler::transferFrom(tokenA, victim, attacker, 10e18) TestERC20::transferFrom(victim, attacker, 10e18) → true

Post-conditions asserted by the test

  • Victim balance decreases by 10e18

  • Attacker balance increases by 10e18

If integrated as stated in scope (preflight match to 0x protocol to block manipulated tokens/senders/amounts/receivers/slippages), this allows direct theft. The Integration PoC demonstrates showss misdirection of funds after the real library returns true.

Was this helpful?