58105 sc medium zeroxswapverifier decodes execute payload with wrong abi bytes vs bytes temporary freezing of funds

Submitted on Oct 30th 2025 at 17:36:01 UTC by @humaira45 for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58105

  • Report Type: Smart Contract

  • Report severity: Medium

  • Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/utils/ZeroXSwapVerifier.sol

  • Impacts:

    • Temporary freezing of funds for at least 24 hour

    • Temporary freezing of funds for at least 1 hour

Description

Brief/Intro

ZeroXSwapVerifier is meant to pre‑validate 0x/Matcha calldata before forwarding it to the 0x Settler/Router. The verifier’s execute branch decodes the payload with the wrong ABI: it expects (SlippageAndActions, bytes) but the canonical 0x Settler uses execute(AllowedSlippage, bytes[] actions, bytes32). As soon as the second parameter (actions) is non‑empty (the normal case), abi.decode reverts inside the verifier, so any path that relies on verifier→forward fails.

We provide an end‑to‑end Foundry PoC where withdraw/deallocate is gated by ZeroXSwapVerifier. Using a valid execute payload with a non‑empty bytes[] actions, verifySwapCalldata reverts deterministically, withdraw fails, and funds remain stuck. The freeze persists after vm.warp +1 hour and +24 hours (time‑independent bug).

Vulnerability Details

What the contract does

  • ZeroXSwapVerifier.verifySwapCalldata decodes a 0x Settler call (execute or executeMetaTxn) and is expected to:

    • Decode the top‑level payload,

    • Parse and verify actions,

    • Return true for valid payloads.

Where the bug happens

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

    • Constants declare:

      • EXECUTE_SELECTOR = 0xcf71ff4f // execute(SlippageAndActions,bytes[])

      • EXECUTE_META_TXN_SELECTOR = 0x0476baab // executeMetaTxn(SlippageAndActions,bytes[],address,bytes)

    • But the decoder for execute is wrong:

      • _verifyExecuteCalldata(bytes data, …) currently:

      • (SlippageAndActions memory saa, ) = abi.decode(data, (SlippageAndActions, bytes)); // WRONG TYPE

      • It should decode (SlippageAndActions, bytes[] actions, bytes32 tag) per 0x Settler.

    • Meta variant decodes (SlippageAndActions, bytes[], address, bytes) correctly.

Why this is a bug (with 0x Settler ABI)

  • Canonical 0x Settler (0xProject/0x-settler):

    • Settler.execute(AllowedSlippage slippage, bytes[] actions, bytes32 tag)

    • SettlerMetaTxn.executeMetaTxn(AllowedSlippage, bytes[] actions, bytes32, address, bytes)

  • Therefore, decoding execute as (SlippageAndActions, bytes) is ABI-incompatible and causes abi.decode to revert on any non‑empty actions array (the normal case). The third parameter (bytes32 tag) is also ignored.

Impact Details

  • Any withdraw/deallocate path that relies on ZeroXSwapVerifier + execute will fail for valid 0x payloads (actions non‑empty) due to the wrong decoder. This temporarily freezes user funds (withdraw reverts) and persists over time until code/params are changed.

References

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

    • Functions: verifySwapCalldata, _verifyExecuteCalldata, _verifyExecuteMetaTxnCalldata

  • 0x Settler (canonical ABI used by 0xProject):

    • Settler.execute(AllowedSlippage, bytes[] actions, bytes32)

    • SettlerMetaTxn.executeMetaTxn(AllowedSlippage, bytes[] actions, bytes32, address, bytes)

https://gist.github.com/humairar301-droid/d923020002f41b291e1489c34a3a8b85

Proof of Concept

Proof of Concept

What this PoC proves (end‑to‑end)

  • A minimal “withdraw router” holds user funds and requires ZeroXSwapVerifier.verifySwapCalldata to pass before releasing tokens (no user‑facing fallback).

  • For a valid execute payload with non‑empty bytes[] actions (as 0x Settler expects), the verifier decodes with the wrong ABI and reverts inside abi.decode.

  • Withdraw therefore reverts and funds stay stuck. After vm.warp +1 hour and +24 hours, withdraw still reverts (time‑independent code bug).

Full PoC (single file) Gist: https://gist.github.com/humairar301-droid/d923020002f41b291e1489c34a3a8b85

Save as: src/test/ZeroX_DecodeBug_Freeze_PoC.t.sol

How to run

  • Medium (≥ 1 hour)

    • forge test -vvv --match-test test_ExecuteDecodeBug_WithdrawFreeze_Persists_1h --evm-version cancun

  • Persistence (≥ 24 hours; not claimed as severity here)

    • forge test -vvv --match-test test_ExecuteDecodeBug_WithdrawFreeze_Persists_24h --evm-version cancun

Representative results

  • test_ExecuteDecodeBug_WithdrawFreeze_Persists_1h: PASS

    • First withdraw → revert from abi.decode inside _verifyExecuteCalldata

    • vm.warp +1h → withdraw still reverts

    • Router still holds user funds; user balance unchanged

  • test_ExecuteDecodeBug_WithdrawFreeze_Persists_24h: PASS

    • Same behavior after vm.warp +24h

Why this is in‑scope and feasible

  • ZeroXSwapVerifier is explicitly in-scope, and the bounty requested special attention that “in-place verification matches 0x protocol logic.”

  • The bug is purely in the verifier’s ABI decoding (not a third-party outage). Any production path that uses execute with actions[].length > 0 is impacted.

  • The PoC demonstrates an end-to-end freeze with no user‑facing fallback. The failure persists over time until code is fixed or configuration is changed.

Suggested remediation

Was this helpful?