58709 sc low naive 0x fill parsing lets attackers spoof token and amount checks

Submitted on Nov 4th 2025 at 07:45:59 UTC by @konvati for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58709

  • 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

Finding Description and Impact

ZeroXSwapVerifier trusts the first 32–64 bytes of opaque 0x fill payloads when validating swap tokens and amounts. For UNISWAPV3_VIP, _extractTokenFromUniswapFills(fills) simply decodes the first word as an address, and for RFQ_VIP, _extractTokenAndAmountFromRFQ(fillData) decodes the first two words as (address, uint256) (src/utils/ZeroXSwapVerifier.sol:214-244arrow-up-right). In the actual 0x Settler ABI, these blobs are complex encodings whose leading words are not guaranteed to be the sell token or amount; the structure is entirely user-controlled.

Because the verifier compares the decoded address against the target token and assumes the decoded amount matches the quoted sell size, an attacker can forge the first words of the fill data to match the expectations while hiding the real parameters deeper in the blob. The naive parser never inspects those deeper values, so _verifyUniswapV3VIP and _verifyRFQVIP accept malicious swaps that trade unapproved tokens or arbitrary amounts.

Integrations that rely on verifySwapCalldata as a preflight safety check (for example, before approving and forwarding calldata to the 0x Settler) lose their token whitelist guarantees. An attacker can submit a malicious quote that passes verification yet executes with a completely different sell token or amount once routed through 0x.

Affected code


Impact

Token whitelist bypass for UniswapV3 VIP swaps

  • _verifyUniswapV3VIP compares the decoded sell token against targetToken.

  • By placing the expected token address in the first word of fills, the attacker satisfies this check even if the rest of the blob drives 0x to sell a different asset.

  • The orchestrator forwards the payload, and 0x executes the swap with the malicious token, violating whitelist assumptions.

Arbitrary sell amounts in RFQ VIP swaps

  • _verifyRFQVIP trusts the first two words of fillData as (sellToken, sellAmount).

  • The attacker sets benign values in those slots, while the actual RFQ order embedded later spends any token/amount the attacker controls.

  • Limit or slippage protections outside the verifier provide no defense because the forged header data is never used by 0x.


  1. Replace the naive _extractTokenFromUniswapFills and _extractTokenAndAmountFromRFQ helpers with full decoders for the 0x Settler fill formats (or query the official 0x libraries) so the verifier inspects the authentic token and amount fields.

  2. Alternatively, require the orchestrator to supply the expected sell token and amount per action, and enforce equality directly without re-parsing the opaque blob.

  3. Add regression tests that attempt to spoof the header words to ensure the verifier now rejects mismatched payloads.


Proof of Concept

PoC Test testSpoofedUniswapV3FillBypassesTokenCheck That will Run in (src/test/Poc.t.sol)

Exploit Sequence 1

  1. Craft UniswapV3 VIP fills bytes where the first word is the allow-listed token but the second word encodes the real malicious token.

  2. Build the execute() payload using this action and ask the verifier to match the allow-listed token.

  3. The verifier decodes only the first word, sees the expected token, and returns true, despite the embedded payload swapping a different token.

Exploit Sequence 2

  1. Encode RFQ fillData where the first two words mimic the expected (token, amount) but place the real (token, amount) deeper in the blob.

  2. Submit this payload to the verifier with the intended target token.

  3. _extractTokenAndAmountFromRFQ decodes the forged header, so verification succeeds while 0x would execute using the hidden parameters.

Test file

Command:

Results

The Forge test logs the spoof and shows the verifier accepting it

Was this helpful?