58348 sc low zeroxswapverifier accepts malicious 0x calldata recipient not bound minout ignored transferfrom misused attacker can route strategy vault funds to self direct theft
Submitted on Nov 1st 2025 at 12:53:46 UTC by @manvi for Audit Comp | Alchemix V3
Report ID: #58348
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
While analysing ZeroXSwapVerifier, I observed that top-level fields (recipient, buyToken, minAmountOut) are not enforced and per-action recipients are not pinned to a safe sink (e.g., the owner/strategy).
I also observed that the verifier allows a raw transferFrom(token, owner, attacker, amount) action as long as token == targetToken.
I analysed three adversarial cases and the verifier returned true for all:
UniswapV3 VIP with recipient = attacker
TRANSFER_FROM with from = owner, to = attacker
basicSellToPool with recipient = attacker and minOut = 0
These behaviours collectively permit an attacker to craft calldata that passes verification and then steals funds when executed by the 0x Settler/Permit2 that holds the owner's allowance.
Vulnerability Details
In the top-level execute verification path (e.g., _verifyExecuteCalldata / _verifyExecuteMetaTxnCalldata), the verifier does not check:
saa.recipient == owner (or other allowed sink)
saa.buyToken == targetToken
saa.minAmountOut > 0
In action verifiers (e.g., _verifyUniswapV3VIP, _verifyTransferFrom, _verifySellToLiquidityProvider, etc.), the code does not bind the actio's recipient/to to owner and does not enforce a safe output target or meaningful slippage bound beyond a BPS cap.
For TRANSFER_FROM, the check effectively reduces to token == targetToken, allowing from = owner and to = attacker to pass.
Affected Code
src/utils/ZeroXSwapVerifier.sol: execute decoders and per-action verifiers (VIP/TRANSFER_FROM/etc.) do not bind recipients or enforce minAmountOut / buyToken invariants.
Attack Path / Scenario
Strategy/vault grants allowance to a 0x Settler or Permit2 (standard for swaps).
Attacker crafts 0x calldata that sets recipient = attacker (or encodes a raw transferFrom(owner, attacker, …)), and uses minOut = 0.
Calldata is passed through ZeroXSwapVerifier.verifySwapCalldata(...) and returns true.
The system proceeds to call the 0x Settler/Permit2 with that calldata; funds move to the attacker or are swapped with unlimited slippage.
Preconditions
The component that moves funds relies on ZeroXSwapVerifier to gate 0x calls.
The strategy/vault (or owner) has token allowance set for the 0x Settler/Permit2 (typical in these designs).
No additional external checks that bind recipient/minOut/buyToken at the integration layer.
Impact Details
Direct loss of user funds from strategies/vaults.
Depending on balances and automation, this could escalate towards protocol insolvency or widespread losses if repeatedly exploited.
References
Contract : src/utils/ZeroXSwapVerifier.sol (branch immunefi_audit)
Proof of Concept
I created a Foundry test file ZeroXSwapVerifier_Drain.t.sol.
My PoC builds malicious 0x actions and wraps them with an EXECUTE_SELECTOR payload. It then calls:
and asserts ok == true. All three adversarial tests PASS, proving the verifier accepts payloads that:
Direct outputs to attacker (arbitrary recipient),
Perform transferFrom(owner, attacker, amount), and
Use minOut = 0 with ignored recipient/buyToken at the top level.
What my POC does :
_wrapExecute uses new bytes for a single action and new bytes trailing array.
Tests that pass (vulnerable behaviour):
test_UniVIP_AllowsArbitraryRecipient_BUG()
test_TransferFrom_ToAttacker_BUG()
test_BasicSellToPool_IgnoresRecipientAndMinOut_BUG()
Content of my POC file :
run my POC file :
My console output :
what my POC proved :
The verifier accepts malicious calldata.
My tests call ZeroXSwapVerifier.verifySwapCalldata(evil, owner, address(token), 1000) and it returns true for payloads that should be rejected. That's the core bug.
Arbitrary recipient is allowed (UniswapV3 VIP).
test_UniVIP_AllowsArbitraryRecipient_BUG() shows I can set recipient = attacker inside the VIP action and the verifier still returns true. -> Proves: recipient isn't bound/pinned to the owner or another safe sink.
Direct transfer to attacker is allowed (TRANSFER_FROM).
test_TransferFrom_ToAttacker_BUG() encodes transferFrom(token, from=owner, to=attacker, amount) and wraps it in an accepted execute payload. Verifier returns true. -> Proves: no check on to, and no requirement that from == owner be the only allowed flow to a safe destination.
Slippage/minOut and outputs aren't enforced (basicSellToPool).
test_BasicSellToPool_IgnoresRecipientAndMinOut_BUG() uses minOut = 0 and recipient = attacker; verifier still returns true. -> Proves: minAmountOut / buyToken aren't enforced at the top level and action outputs aren't bound.
In a real integration, once a vault/strategy has given allowance to the 0x Settler/Permit2, any calldata that passes this verifier will be executed. Since my PoC shows the verifier accepts attacker-chosen recipients and raw transferFrom(owner->attacker), an attacker can steal funds or force unbounded slippage.
Was this helpful?