58480 sc low missing recipient and token binding in verifyswapcalldata leads to unauthorized fund transfers

#58480 [SC-Low] Missing recipient and token binding in verifySwapCalldata leads to unauthorized fund transfers

Submitted on Nov 2nd 2025 at 16:16:11 UTC by @IShiftOnBlue for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58480

  • 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 contract performs validation on calldata intended for 0x-style swap executions. However, its verifySwapCalldata() function does not properly bind critical parameters such as recipient, buyToken, and router. As a result, a crafted calldata can be marked as valid even when it encodes a direct token transfer (transferFrom) to an arbitrary external address. If deployed in production, this logic gap would allow unauthorized movement of user-approved tokens by any contract relying on this verifier as a pre-swap safeguard.

Vulnerability Details

ZeroXSwapVerifier.verifySwapCalldata() accepts arbitrary payloads and superficially checks their structure, but omits key integrity constraints.

  • Router not constrained — the function accepts any "to" address without enforcing an allowlist or verifying it matches a known router.

  • Missing parameter validation — fields like recipient, buyToken, and minAmountOut are never checked or compared against expected values.

  • TRANSFER_FROM path incomplete — _verifyTransferFrom() verifies only that token == targetToken, ignoring from, to, and amount. This means that transferFrom(owner, arbitraryEOA, amount) can pass verification if the token matches the target.

  • VIP parsing leniency — the UniswapV3 and RFQ VIP decoders extract placeholders without enforcing semantic meaning, allowing spoofed recipient and buyToken.

  • Fail-open edge case — when calldata length is under 4 bytes, the function returns false without reverting. If the caller does not assert the verifier result, verification can be silently bypassed.

Together, these issues make the verifier permissive instead of protective. Any protocol or vault that relies on verifySwapCalldata() to approve outgoing calls could end up executing payloads that directly move tokens to unintended addresses.

Example snippet (illustrative):

Impact Details

If this verifier were used on mainnet within any swap or vault orchestration logic, an attacker could:

  • Craft a 0x payload that passes verification but encodes a transferFrom directly to an arbitrary address.

  • Execute the payload through a permitted router or strategy that has spending approval from users or vaults.

  • Move ERC20 tokens out of the contract or user account without reverting or detection.

Impact type: Direct theft of user funds (at rest or in motion).

Severity rationale: The verifier’s purpose is to prevent unauthorized execution. Its failure in this context inverts that security guarantee, converting an intended safeguard into a vector for deterministic fund movement. This qualifies as a critical impact under the program’s definitions.

References

  • Vulnerable contract: src/utils/ZeroXSwapVerifier.sol (code in repository)

  • Foundry logs confirming unauthorized transfer scenario (combined logs available)

  • Environment: Solc 0.8.28 — Foundry 1.4.3 (Ubuntu WSL)

Proof of Concept

Proof of Concept — ZeroXSwapVerifier (Direct Theft via TRANSFER_FROM)

Environment

  • Foundry: 1.4.3 (stable)

  • Solidity: 0.8.28

  • Local run; no RPC/forks required

How to Run

  1. Save the code block below as: test/ZeroXSwapVerifier_TransferFrom_PoC.t.sol

  2. Build: forge build

  3. Run (pick one):

    • Linux/WSL: ~/.foundry/bin/forge test --match-path test/ZeroXSwapVerifier_TransferFrom_PoC.t.sol -vv

    • Windows: .bin/forge.exe test --root . --config-path foundry.toml --match-path test/ZeroXSwapVerifier_TransferFrom_PoC.t.sol -vv

Expected Result

  • verifySwapCalldata(...) returns true for attacker-controlled recipient and buyToken.

  • After execute(...), attacker balance increases by amount and harness balance decreases by amount (single-tx drain).

PoC Test Code (copy/paste as-is into test/ZeroXSwapVerifier_TransferFrom_PoC.t.sol)

// SPDX-License-Identifier: MIT pragma solidity ^0.8.28;

import "forge-std/Test.sol"; import {ZeroXSwapVerifier} from "src/utils/ZeroXSwapVerifier.sol";

// Minimal ERC20 token used to credit the harness and observe the drain. contract MockERC20 { string public name = "Mock"; string public symbol = "MCK"; uint8 public decimals = 18; uint256 public totalSupply;

}

contract PermissiveSettler { event ActionExecuted(address token, address from, address to, uint256 amount);

}

contract SwapExecutorHarness { MockERC20 public immutable token; PermissiveSettler public immutable settler; uint256 public immutable maxSlippageBps;

}

contract ZeroXSwapVerifier_TransferFrom_PoC is Test { MockERC20 token; PermissiveSettler settler; SwapExecutorHarness harness;

}

Run command (WSL, Foundry 1.4.3 stable)

wsl -d Ubuntu -e bash -lc 'cd /home/manii/v3-poc-immunefi_audit/poc && FOUNDRY_ALLOW_PATHS="["../"]" ~/.foundry/bin/forge test --match-path test/ZeroXSwapVerifier_TransferFrom_PoC.t.sol -vv'

Output (excerpt)

Compiling 26 files with Solc 0.8.28 Ran 1 test for test/ZeroXSwapVerifier_TransferFrom_PoC.t.sol:ZeroXSwapVerifier_TransferFrom_POCISO [PASS] test_PoC_TransferFrom_ToEOA_Verified_And_Drains() (gas: 240552) Suite result: ok. 1 passed; 0 failed; 0 skipped

Notes

  • Root cause: verifySwapCalldata does not bind recipient/buyToken and does not restrict direct ERC20 selectors (transfer / transferFrom) inside actions.

  • The crafted payload passes verification and routes funds to an attacker-controlled EOA, allowing a single-transaction drain.

Was this helpful?