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 V3
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
Save the code block below as:
test/ZeroXSwapVerifier_TransferFrom_PoC.t.solBuild:
forge buildRun (pick one):
Linux/WSL:
~/.foundry/bin/forge test --match-path test/ZeroXSwapVerifier_TransferFrom_PoC.t.sol -vvWindows:
.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-controlledrecipientandbuyToken.After
execute(...), attacker balance increases byamountand harness balance decreases byamount(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:
verifySwapCalldatadoes not bindrecipient/buyTokenand does not restrict direct ERC20 selectors (transfer/transferFrom) insideactions.The crafted payload passes verification and routes funds to an attacker-controlled EOA, allowing a single-transaction drain.
Was this helpful?