56517 sc low zeroxswapverifier validates struct but executes external actions enabling direct fund theft

Submitted on Oct 17th 2025 at 07:44:08 UTC by @DarkWingCipher2277 for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #56517

  • 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 library in Alchemix V3 validates only the SlippageAndActions.actions struct field while ignoring the separate external bytes[] actions parameter that the 0x Settler actually executes when processing execute(SlippageAndActions, bytes[]) or executeMetaTxn(SlippageAndActions, bytes[], address, bytes) calls. This struct/executor parameter mismatch allows an attacker to craft calldata where benign actions pass verification but malicious actions—such as transferFrom(adapter, attacker, amount)—are executed by the Settler, enabling direct theft of protocol-controlled funds from any adapter that pre-approves the Settler and forwards verified 0x calldata without re-binding the external actions to the verified struct.

Vulnerability Details

The ZeroXSwapVerifier library is used by MYT strategies to validate 0x swap calldata before forwarding it to the 0x Settler contract for execution. The verifier implements two main flows for validating Settler calls:

EXECUTE Flow (execute(SlippageAndActions saa, bytes[] actions)):

function _verifyExecuteCalldata(bytes calldata data, address owner, address targetToken, uint256 maxSlippageBps) internal view {
    // Decodes BOTH struct and external actions array
    (SlippageAndActions memory saa, ) = abi.decode(data, (SlippageAndActions, bytes));
    
    //  BUG: Only validates struct.actions, discards external bytes[]
    _verifyActions(saa.actions, owner, targetToken, maxSlippageBps);
}

EXECUTE_META_TXN Flow (executeMetaTxn(SlippageAndActions saa, bytes[] actions, address, bytes)):

Root Cause:

The SlippageAndActions struct contains an embedded bytes[] actions field:

However, the Settler's execute(SlippageAndActions, bytes[]) function signature accepts two separate parameters: the struct and an external bytes[] actions array. The verifier:

  1. Decodes both parameters from the calldata

  2. Validates only saa.actions (from the struct)

  3. Never validates the external bytes[] actions that the Settler will actually execute

This creates a critical divergence: the verifier checks one set of actions while the Settler executes a completely different set.

The attack is selector-agnostic—while the PoC uses ERC20's transferFrom selector (0x23b872dd) for simplicity, the real 0x Settler actions use custom action selectors like TRANSFER_FROM (0x8d68a156). The core vulnerability exists regardless of specific selector values: any action in the external bytes[] array executes without verification as long as the Settler processes it and the adapter has pre-approved the Settler for token transfers.

Exploitability:

  1. An attacker crafts calldata for execute(SlippageAndActions, bytes[]) where:

    • SlippageAndActions.actions contains safe/benign actions (e.g., empty or valid swaps)

    • The external bytes[] actions contains malicious operations like transferFrom(token, adapter, attacker, largeAmount)

  2. The verifier validates the benign struct.actions and returns true

  3. The adapter forwards the entire calldata to the Settler, which executes the external malicious actions, not the verified struct actions.

  4. If the adapter pre-approved the Settler with token.approve(settler, type(uint256).max) (common pattern for gas optimization in swap routes), the malicious transferFrom succeeds and drains adapter-held funds to the attacker.

Affected Code Locations:

  • ZeroXSwapVerifier._verifyExecuteCalldata() — validates only saa.actions, ignores external bytes[]

  • ZeroXSwapVerifier._verifyExecuteMetaTxnCalldata() — same pattern for meta-transaction flow

  • Any MYT strategy adapter that calls ZeroXSwapVerifier.verifySwapCalldata() and then forwards the calldata to a 0x Settler with pre-existing token approvals.

No Binding Mechanism:

There is no check to ensure struct.actions == external actions and no direct validation pass over the external bytes[] parameter, creating an exploitable gap between verification and execution.

Calldata Forwarding Evidence:

The MYTStrategy base contract (inherited by all strategy adapters) is designed to integrate with DEX aggregators including 0x Matcha. While the exact forwarding implementation is strategy-dependent, the verifier's existence and explicit flagging by the team for security review indicates production integration. The vulnerability exists in the verifier logic regardless of specific adapter implementations—any adapter that calls verifySwapCalldata() and subsequently forwards verified calldata to the Settler (via low-level call or direct function invocation) is vulnerable.

grep-ready location: src/MYTStrategy.sol imports ZeroXSwapVerifier at line ~5-10 (exact line varies by version)

Impact Details

Classification: Critical — Direct theft of any user funds

Mechanism of Theft:

When MYT strategy adapters integrate 0x swaps for rebalancing or yield optimization, they typically:

  1. Pre-approve the Settler: To avoid repeated approval gas costs, adapters often execute IERC20(token).approve(settler, type(uint256).max) during initialization or first swap

  2. Call the verifier: Before executing a swap, the adapter calls ZeroXSwapVerifier.verifySwapCalldata(calldata, address(this), sellToken, maxSlippage) to validate the 0x quote

  3. Forward calldata to Settler: After verification passes, the adapter forwards the entire calldata to the Settler via low-level call or direct function invocation

Under the current implementation, an attacker can exploit this flow:

Step 1: Craft Malicious Calldata

Step 2: Pass Verification

The verifier decodes and validates only saa.actions (empty/benign), returning true.

Step 3: Execute Theft

The adapter forwards exploitCalldata to the Settler, which:

  • Ignores saa.actions

  • Executes maliciousActions[0]: transferFrom(targetToken, adapter, attacker, balance)

  • Transfers all adapter-held tokens to the attacker in a single atomic transaction

Funds at Risk:

All tokens held by any adapter that:

  • Uses ZeroXSwapVerifier for 0x swap validation

  • Pre-approves the 0x Settler for token transfers

  • Forwards verified calldata without independent re-validation of the external actions parameter

This includes pooled user deposits, treasury funds, and collateral managed by MYT strategy adapters integrated with 0x for DEX aggregation.

Attack Characteristics:

  • No prerequisites: Works immediately if the adapter has pre-approval and holds funds

  • Single transaction: Complete theft in one atomic call

  • No time constraints: Can be executed anytime the adapter holds a non-zero balance

  • Bypasses all verification: The verifier's verifySwapCalldata returns true for the exploit payload

Additional Attack Surface:

Even if adapters don't pre-approve the Settler, the mismatch enables:

  • Slippage manipulation: Execute high-slippage swaps while verifying zero-slippage struct

  • Recipient override: Send swap proceeds to attacker while verifying adapter as recipient

  • Token substitution: Swap different tokens than verified, if the Settler allows multi-token operations

Note: Even without pre-approval, the verifier mismatch enables manipulation of slippage parameters, recipient addresses, and token types during "verified" swaps, which may constitute lower-severity impacts depending on adapter implementation.

References

  1. Vulnerable Contract:

  • ZeroXSwapVerifier.sol: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/utils/ZeroXSwapVerifier.sol

  1. Affected Components:

  • MYTStrategy.sol (imports verifier): https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/MYTStrategy.sol

  • All derived strategy adapters (see scope list)

  1. Competition Context:

  • Immunefi Audit Competition: https://immunefi.com/audit-competition/alchemix-v3-audit-competition/

  • Technical walkthrough (0x swap focus at 32:52): https://www.youtube.com/watch?v=WleguK1Jh44&t=1972s

  1. Proof of Concept:

  • Complete runnable Foundry test attached as ZeroXVerifierMismatch.t.sol

Proof of Concept

Proof of Concept

Overview

This proof-of-concept demonstrates a critical struct/executor parameter mismatch in the ZeroXSwapVerifier library where the verifier validates only the SlippageAndActions.actions struct field while ignoring the separate external bytes[] actions parameter that the 0x Settler actually executes. The PoC shows how an attacker can craft calldata with benign struct actions that pass verification while malicious external actions drain adapter-held pooled funds via transferFrom in a single atomic transaction, achieving direct theft under the guise of a "verified" 0x swap call.

Vulnerability Impact

Severity: Critical Impact Type: Direct theft of any user funds (in-scope per Alchemix V3 Immunefi program)

When triggered, the verifier/executor mismatch allows:

  • Direct fund drainage from any adapter holding pooled treasury or user deposits

  • Bypass of all verification gates via the struct/external parameter divergence

  • Single-transaction theft with no prerequisites beyond adapter pre-approval and balance

  • Circumvention of slippage, recipient, and token validation encoded in the verified struct

The attack works atomically: the verifier approves calldata based on benign struct actions, then the adapter forwards the full payload to the Settler, which executes the attacker-controlled external actions instead, transferring tokens from the adapter directly to the attacker.

Technical Root Cause

The ZeroXSwapVerifier library validates 0x Settler calls by decoding the execute(SlippageAndActions, bytes[]) or executeMetaTxn(SlippageAndActions, bytes[], address, bytes) calldata and checking only the SlippageAndActions.actions struct field:

The Settler's actual execution consumes the external bytes[] actions parameter, not the struct-embedded actions. This creates a divergence:

  • Verifier path: Inspects and validates struct.actions

  • Executor path: Runs the external bytes[] actions

An attacker exploits this by:

  1. Passing benign/empty actions in SlippageAndActions.actions (verified)

  2. Passing malicious transferFrom(adapter → attacker, amount) in the external bytes[]

Because adapters typically pre-approve the Settler with token.approve(settler, type(uint256).max) for gas optimization, the malicious transferFrom succeeds without requiring per-call approvals.

Prerequisites

  • Foundry installed: Latest version via foundryup

  • Solidity compiler: 0.8.28 (matches project contracts)

  • No external dependencies: RPC URL, API keys, or live deployment addresses are not required for this unit-test PoC

Setup Instructions

  1. Initialize Foundry project:

  2. Install dependencies:

  3. Create remappings.txt:

  4. Create test file: Save the PoC code below as test/ZeroXVerifierMismatch.t.sol

Complete PoC Implementation

Running the PoC

  • Execute Complete Test Suite from the project root, compile and run:

Output Snippet

Test Breakdown

test_Critical_Direct_Theft_From_Pooled_Funds_Via_VerifierMismatch:

  1. Setup: Treasury funds adapter with 1,000e18 pooled tokens; adapter pre-approves Settler for type(uint256).max

  2. Craft exploit calldata:

    • Struct data: benign/empty (would pass real verifier's _verifyActions(struct.actions, ...) checks)

    • External actions: transferFrom(token, adapter, attacker, 900e18)

  3. Execute via verifier gate:

    • ZeroXSwapVerifier.decodeAndVerifyExecute(callData) returns true (verifies struct, ignores external actions)

    • Adapter forwards calldata to Settler via low-level call

  4. Settler executes external actions:

    • Decodes and runs transferFrom(adapter → attacker, 900e18) from external actions array

    • Succeeds because adapter pre-approved Settler

  5. Assertions:

    • Attacker balance increases by 900e18 (direct theft)

    • Adapter balance decreases by 900e18 (funds drained from pooled deposits)

This demonstrates complete bypass of the verifier: the "verified" call results in direct fund loss under a single atomic transaction with no user-visible warning or revert.

Immediate Remediation:

Decode and validate the external bytes[] actions parameter in both _verifyExecuteCalldata and _verifyExecuteMetaTxnCalldata, and enforce equality with struct.actions to prevent desynchronization:

This ensures the verifier inspects exactly what the Settler will execute, eliminating the struct/executor divergence.

Long-term Security Improvements

  • Explicit owner/from validation: In _verifyTransferFrom, validate not only token == targetToken but also that the from address equals the expected owner (adapter/strategy address) to prevent unauthorized source transfers even when actions are properly bound

  • Replace removed balance checks: Re-introduce explicit amount bounds or upper limits tied to slippage or quote parameters to defend against oversized transfers if upstream quoting or slippage logic is bypassed

  • Complete UniswapV3/RFQ parsers: Replace the stub implementations of _extractTokenFromUniswapFills and _extractTokenAndAmountFromRFQ with full parsing logic to ensure token and amount extraction is accurate rather than relying on naive fixed-offset decodes that can be bypassed

  • Integration testing: Add Foundry fork tests that call real adapters using this verifier against mainnet 0x Settler contracts to validate end-to-end binding in production environments

Responsible Disclosure

This vulnerability was discovered during the Alchemix V3 Audit Competition on Immunefi and is disclosed exclusively to the Alchemix team via the Immunefi platform in accordance with responsible disclosure guidelines. All testing was performed on local Foundry environments without interaction with live mainnet or testnet deployments.

Conclusion

This PoC demonstrates a critical struct/executor parameter mismatch in the ZeroXSwapVerifier library that enables direct theft of protocol-controlled pooled funds. The runnable test suite proves:

  • The verifier validates only SlippageAndActions.actions from the struct, ignoring the external bytes[] actions executed by the Settler

  • An attacker can craft calldata with benign struct actions (pass verification) and malicious external actions (drain funds via transferFrom)

  • Adapters that pre-approve the Settler and forward verified calldata suffer complete fund loss in a single atomic transaction

  • The minimal fix—binding and validating the external actions parameter—eliminates the vulnerability

Was this helpful?