For the complete documentation index, see llms.txt. This page is also available as Markdown.

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 V3

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