ERC-7579 Vulnerability Patterns

December 23, 2025

ERC-7579 (Minimal Modular Smart Accounts) defines a standard interface for modular smart accounts that can install and use various modules (validators, executors, hooks, fallback handlers). It enables extensible account functionality while maintaining interoperability across the ecosystem.

This article summarizes ERC-7579 vulnerability patterns with real audit cases to help you understand the security implications.

ERC-7579 Core Concepts

What is ERC-7579?

┌─────────────────────────────────────────────────────────────────┐
│                    ERC-7579 Modular Smart Account               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Purpose:                                                       │
│  Standardize interfaces for modular smart accounts to enable   │
│  interoperability. Modules can work across different account   │
│  implementations, avoiding vendor lock-in.                      │
│                                                                 │
│  Background - Why Modular Accounts?                             │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                                                         │   │
│  │  Traditional EOA (Externally Owned Account):            │   │
│  │  • Fixed functionality: only sign transactions          │   │
│  │  • Single private key controls everything               │   │
│  │  • Cannot add custom logic                              │   │
│  │                                                         │   │
│  │  Smart Contract Account (e.g., Safe, Nexus):            │   │
│  │  • Programmable: can add any logic                      │   │
│  │  • Multi-sig, social recovery, spending limits, etc.    │   │
│  │  • But: different implementations are incompatible      │   │
│  │                                                         │   │
│  │  ERC-7579 Solution:                                     │   │
│  │  • Define standard module interfaces                    │   │
│  │  • A "Validator" module for Safe can also work on Nexus │   │
│  │  • Ecosystem of reusable, audited modules               │   │
│  │                                                         │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                    Core Interfaces Explained                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. IERC7579Execution - How account executes transactions       │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                                                         │   │
│  │  execute(mode, executionData)                           │   │
│  │  ┌─────────────────────────────────────────────────┐   │   │
│  │  │ What: Account's main function to do things      │   │   │
│  │  │                                                 │   │   │
│  │  │ Example: User wants to swap ETH for USDC       │   │   │
│  │  │ → Wallet calls account.execute(...)             │   │   │
│  │  │ → Account calls Uniswap with user's assets     │   │   │
│  │  │                                                 │   │   │
│  │  │ mode: Defines HOW to execute                   │   │   │
│  │  │ • Single call vs batch calls                   │   │   │
│  │  │ • Try/catch or revert on failure               │   │   │
│  │  │ • Delegatecall or regular call                 │   │   │
│  │  └─────────────────────────────────────────────────┘   │   │
│  │                                                         │   │
│  │  executeFromExecutor(mode, executionData)               │   │
│  │  ┌─────────────────────────────────────────────────┐   │   │
│  │  │ What: Same as execute(), but for Executor module│   │   │
│  │  │                                                 │   │   │
│  │  │ Why separate? Access control                   │   │   │
│  │  │ • execute() → only owner/validator can call    │   │   │
│  │  │ • executeFromExecutor() → installed Executor   │   │   │
│  │  │   modules can also call                        │   │   │
│  │  │                                                 │   │   │
│  │  │ Example: DCA bot (Executor module) can         │   │   │
│  │  │ automatically execute trades without user      │   │   │
│  │  │ signing each transaction                       │   │   │
│  │  └─────────────────────────────────────────────────┘   │   │
│  │                                                         │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  2. IERC7579AccountConfig - Account capabilities query          │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                                                         │   │
│  │  accountId() → string                                   │   │
│  │  ┌─────────────────────────────────────────────────┐   │   │
│  │  │ Returns: "vendor.account.semver"                │   │   │
│  │  │ Example: "biconomy.nexus.1.0.0"                 │   │   │
│  │  │                                                 │   │   │
│  │  │ Why: Identify account implementation            │   │   │
│  │  │ • Module can check compatibility                │   │   │
│  │  │ • UI can show correct features                  │   │   │
│  │  └─────────────────────────────────────────────────┘   │   │
│  │                                                         │   │
│  │  supportsExecutionMode(mode) → bool                     │   │
│  │  ┌─────────────────────────────────────────────────┐   │   │
│  │  │ Check if account supports a specific execution │   │   │
│  │  │ mode before trying to use it                   │   │   │
│  │  │                                                 │   │   │
│  │  │ Example: Before batch execution                 │   │   │
│  │  │ if (!account.supportsExecutionMode(BATCH)) {    │   │   │
│  │  │     // fall back to single calls               │   │   │
│  │  │ }                                              │   │   │
│  │  └─────────────────────────────────────────────────┘   │   │
│  │                                                         │   │
│  │  supportsModule(moduleTypeId) → bool                    │   │
│  │  ┌─────────────────────────────────────────────────┐   │   │
│  │  │ Check if account supports a module type        │   │   │
│  │  │                                                 │   │   │
│  │  │ Example: Not all accounts support Hooks        │   │   │
│  │  │ if (!account.supportsModule(4)) {  // Hook=4   │   │   │
│  │  │     // cannot install this hook module         │   │   │
│  │  │ }                                              │   │   │
│  │  └─────────────────────────────────────────────────┘   │   │
│  │                                                         │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  3. IERC7579ModuleConfig - Install/uninstall modules           │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                                                         │   │
│  │  installModule(typeId, module, initData)                │   │
│  │  ┌─────────────────────────────────────────────────┐   │   │
│  │  │ Add a new module to the account                 │   │   │
│  │  │                                                 │   │   │
│  │  │ typeId: 1=Validator, 2=Executor, 3=Fallback, 4=Hook│ │   │
│  │  │ module: Address of the module contract          │   │   │
│  │  │ initData: Configuration for the module          │   │   │
│  │  │                                                 │   │   │
│  │  │ Process:                                        │   │   │
│  │  │ 1. Account stores module in its registry        │   │   │
│  │  │ 2. Account calls module.onInstall(initData)     │   │   │
│  │  │ 3. Module initializes with account's context    │   │   │
│  │  │                                                 │   │   │
│  │  │ Note: "registry" here means Account's internal  │   │   │
│  │  │ storage that tracks installed modules:          │   │   │
│  │  │ ┌───────────────────────────────────────────┐   │   │   │
│  │  │ │ Account Storage:                          │   │   │   │
│  │  │ │   validators = [0xAAA, 0xBBB]             │   │   │   │
│  │  │ │   executors  = [0xCCC]                    │   │   │   │
│  │  │ │   hook       = 0xDDD                      │   │   │   │
│  │  │ └───────────────────────────────────────────┘   │   │   │
│  │  │                                                 │   │   │
│  │  │ This is different from ERC-7484 Registry        │   │   │
│  │  │ (a global contract that verifies if modules     │   │   │
│  │  │  are audited/attested - see ERC-7484 article)   │   │   │
│  │  │                                                 │   │   │
│  │  │ Example: Install a 2FA validator                │   │   │
│  │  │ account.installModule(                          │   │   │
│  │  │     1,  // Validator type                       │   │   │
│  │  │     twoFactorValidator,                         │   │   │
│  │  │     abi.encode(myGoogleAuthPublicKey)           │   │   │
│  │  │ );                                              │   │   │
│  │  └─────────────────────────────────────────────────┘   │   │
│  │                                                         │   │
│  │  uninstallModule(typeId, module, deInitData)            │   │
│  │  ┌─────────────────────────────────────────────────┐   │   │
│  │  │ Remove a module from the account                │   │   │
│  │  │                                                 │   │   │
│  │  │ Process:                                        │   │   │
│  │  │ 1. Account calls module.onUninstall(deInitData) │   │   │
│  │  │ 2. Module cleans up its state                   │   │   │
│  │  │ 3. Account removes module from registry         │   │   │
│  │  └─────────────────────────────────────────────────┘   │   │
│  │                                                         │   │
│  │  isModuleInstalled(typeId, module, ...) → bool          │   │
│  │  ┌─────────────────────────────────────────────────┐   │   │
│  │  │ Check if a specific module is currently active │   │   │
│  │  │                                                 │   │   │
│  │  │ Use case: Before executing, verify the         │   │   │
│  │  │ required module is still installed             │   │   │
│  │  └─────────────────────────────────────────────────┘   │   │
│  │                                                         │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                    Mandatory ERC Requirements                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ERC-165: Interface Detection                                   │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                                                         │   │
│  │  Problem: How do other contracts know what this         │   │
│  │  contract can do?                                       │   │
│  │                                                         │   │
│  │  Solution: Standard query function                      │   │
│  │  ┌─────────────────────────────────────────────────┐   │   │
│  │  │ function supportsInterface(bytes4 interfaceId)  │   │   │
│  │  │     external view returns (bool);               │   │   │
│  │  │                                                 │   │   │
│  │  │ // Example usage:                               │   │   │
│  │  │ if (account.supportsInterface(IERC7579.id)) {   │   │   │
│  │  │     // This is an ERC-7579 account, safe to use │   │   │
│  │  │ }                                               │   │   │
│  │  └─────────────────────────────────────────────────┘   │   │
│  │                                                         │   │
│  │  Why required:                                          │   │
│  │  • Module registries verify account compatibility      │   │
│  │  • Factories check before deploying                    │   │
│  │  • dApps detect account capabilities                   │   │
│  │                                                         │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  ERC-1271: Signature Validation for Contracts                   │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                                                         │   │
│  │  Problem: Smart contracts don't have private keys,      │   │
│  │  so they can't sign messages like EOAs do.             │   │
│  │                                                         │   │
│  │  Example scenario:                                      │   │
│  │  1. dApp asks: "Sign this message to prove ownership"  │   │
│  │  2. EOA: Uses private key to sign ✓                    │   │
│  │  3. Smart Account: Has no private key ✗                │   │
│  │                                                         │   │
│  │  Solution: Smart account validates signature internally │   │
│  │  ┌─────────────────────────────────────────────────┐   │   │
│  │  │ function isValidSignature(                      │   │   │
│  │  │     bytes32 hash,      // The message hash      │   │   │
│  │  │     bytes signature    // Signature to verify   │   │   │
│  │  │ ) external view returns (bytes4 magicValue);    │   │   │
│  │  │                                                 │   │   │
│  │  │ Returns:                                        │   │   │
│  │  │ • 0x1626ba7e → Signature is valid               │   │   │
│  │  │ • Anything else → Signature is invalid          │   │   │
│  │  └─────────────────────────────────────────────────┘   │   │
│  │                                                         │   │
│  │  How it works in ERC-7579:                              │   │
│  │  1. dApp sends signature + hash to smart account       │   │
│  │  2. Account forwards to installed Validator module      │   │
│  │  3. Validator checks if signature is from owner        │   │
│  │  4. Returns 0x1626ba7e if valid                        │   │
│  │                                                         │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                    Module Types Explained                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Validator (TYPE_ID = 1)                                        │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                                                         │   │
│  │  Purpose: Decide if a transaction is authorized         │   │
│  │                                                         │   │
│  │  Called during: UserOperation validation (ERC-4337)     │   │
│  │                                                         │   │
│  │  Examples:                                               │   │
│  │  • ECDSA Validator: Check if signed by owner's key      │   │
│  │  • Multisig Validator: Require 2-of-3 signatures       │   │
│  │  • Session Key: Allow limited actions for dApps        │   │
│  │  • Passkey Validator: Use WebAuthn/FaceID              │   │
│  │                                                         │   │
│  │  Security note: Most critical module type!             │   │
│  │  A malicious validator = attacker controls your funds   │   │
│  │                                                         │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  Executor (TYPE_ID = 2)                                         │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                                                         │   │
│  │  Purpose: Perform actions on behalf of the account      │   │
│  │                                                         │   │
│  │  Special privilege: Can call executeFromExecutor()      │   │
│  │                                                         │   │
│  │  Examples:                                               │   │
│  │  • DCA Bot: Auto-buy ETH every day                      │   │
│  │  • Limit Order: Execute when price hits target         │   │
│  │  • Recovery Module: Transfer assets if owner inactive  │   │
│  │  • Automation: Claim rewards, rebalance portfolio      │   │
│  │                                                         │   │
│  │  Security note: Can drain your account if malicious!   │   │
│  │                                                         │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  Fallback Handler (TYPE_ID = 3)                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                                                         │   │
│  │  Purpose: Handle calls to functions not in the account │   │
│  │                                                         │   │
│  │  How it works:                                          │   │
│  │  1. Someone calls account.someFunction()               │   │
│  │  2. Account doesn't have someFunction()                │   │
│  │  3. Fallback handler receives the call                 │   │
│  │  4. Handler processes and returns result               │   │
│  │                                                         │   │
│  │  Examples:                                               │   │
│  │  • ERC-721 receiver: Handle NFT transfers              │   │
│  │  • ERC-1155 receiver: Handle multi-token transfers    │   │
│  │  • Custom callbacks: Protocol-specific handlers        │   │
│  │                                                         │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  Hook (TYPE_ID = 4)                                             │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                                                         │   │
│  │  Purpose: Run code before and after execution          │   │
│  │                                                         │   │
│  │  Flow:                                                  │   │
│  │  ┌─────────────────────────────────────────────────┐   │   │
│  │  │ User tx → preCheck() → execute() → postCheck() │   │   │
│  │  │              ↑                        ↑        │   │   │
│  │  │           Hook runs              Hook runs     │   │   │
│  │  └─────────────────────────────────────────────────┘   │   │
│  │                                                         │   │
│  │  Examples:                                               │   │
│  │  • Spending Limit: Block tx if exceeds daily limit     │   │
│  │  • Whitelist: Only allow transfers to approved addrs   │   │
│  │  • Time Lock: Block tx outside business hours          │   │
│  │  • Gas Sponsor: Pay gas for user in postCheck         │   │
│  │                                                         │   │
│  │  Security note: Hook runs in account's context!        │   │
│  │  Can access/transfer approved assets                   │   │
│  │                                                         │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  Note: A single module contract can implement multiple types!  │
│  Example: A module could be both Validator AND Hook            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                    Module Lifecycle                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Install Flow:                                                  │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                                                         │   │
│  │  User                    Account                Module │   │
│  │    │                        │                      │    │   │
│  │    │  installModule(...)    │                      │    │   │
│  │    │───────────────────────>│                      │    │   │
│  │    │                        │                      │    │   │
│  │    │                        │   onInstall(data)    │    │   │
│  │    │                        │─────────────────────>│    │   │
│  │    │                        │                      │    │   │
│  │    │                        │   Module stores its  │    │   │
│  │    │                        │   config for this    │    │   │
│  │    │                        │   account            │    │   │
│  │    │                        │<─────────────────────│    │   │
│  │    │                        │                      │    │   │
│  │    │                        │  Account registers   │    │   │
│  │    │                        │  module in storage   │    │   │
│  │    │                        │                      │    │   │
│  │    │         Done           │                      │    │   │
│  │    │<───────────────────────│                      │    │   │
│  │                                                         │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  Usage - Module executes with account context:                  │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                                                         │   │
│  │  When module code runs:                                 │   │
│  │  • msg.sender = the smart account address              │   │
│  │  • Module can transfer account's approved assets       │   │
│  │  • Module can call contracts as the account            │   │
│  │                                                         │   │
│  │  This is powerful but DANGEROUS if module is malicious!│   │
│  │                                                         │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Vulnerability Patterns

1. Missing ERC-165 Implementation

Prerequisite Knowledge:

ERC-7579 explicitly requires that “Smart accounts MUST implement ERC-165”. This means the account contract must include the standard supportsInterface(bytes4 interfaceId) function that returns true for supported interfaces. This is mandatory for ecosystem interoperability.

Root Cause:

The developers implemented the modular account functionality but overlooked the mandatory ERC-165 interface requirement specified in ERC-7579. The contract lacks the supportsInterface function entirely, violating the standard’s compliance rules.

Attack Vector:

Other contracts or systems attempting to interact with this account using the ERC-7579 standard will fail when they call supportsInterface to check for interface support. This breaks interoperability and prevents proper integration with the broader ERC-7579 ecosystem, including module registries, factory contracts, and other infrastructure that relies on interface detection.

Source:

  • [codehawks/2024-07-biconomy] Protocol not fully compliant with EIP-7579 (M-03.md)

    Business Scenario:

    ERC-165 Interface Detection
    ┌────────────────────────────────────────────────────────────────┐
    │                                                                │
    │  What is ERC-165?                                              │
    │  A standard way for contracts to declare which interfaces     │
    │  they support. Other contracts call supportsInterface() to    │
    │  check before interacting.                                     │
    │                                                                │
    │  Why ERC-7579 requires it:                                     │
    │  • Module registries need to verify account compatibility     │
    │  • Factory contracts check interface support before deploy    │
    │  • Cross-protocol integrations rely on interface detection    │
    │                                                                │
    │  Standard pattern:                                             │
    │  ┌──────────────────────────────────────────────────────────┐ │
    │  │  function supportsInterface(bytes4 interfaceId)          │ │
    │  │      external view returns (bool) {                      │ │
    │  │      return                                              │ │
    │  │          interfaceId == type(IERC165).interfaceId ||     │ │
    │  │          interfaceId == type(IAccount).interfaceId ||    │ │
    │  │          interfaceId == type(IAccountExecute).interfaceId│ │
    │  │  }                                                       │ │
    │  └──────────────────────────────────────────────────────────┘ │
    │                                                                │
    └────────────────────────────────────────────────────────────────┘
    
    Impact:
    ┌────────────────────────────────────────────────────────────────┐
    │                                                                │
    │  Without ERC-165:                                              │
    │  → Module registries cannot verify account type                │
    │  → Factory contracts may reject the account                    │
    │  → Third-party integrations fail silently or revert           │
    │  → Account cannot participate in ERC-7579 ecosystem           │
    │                                                                │
    └────────────────────────────────────────────────────────────────┘
    
    Mitigation:
    ┌────────────────────────────────────────────────────────────────┐
    │                                                                │
    │  Implement ERC-165 in the account contract:                    │
    │                                                                │
    │  contract Nexus is IERC165, ... {                              │
    │      function supportsInterface(bytes4 interfaceId)            │
    │          external pure override returns (bool) {               │
    │          return                                                │
    │              interfaceId == type(IERC165).interfaceId ||       │
    │              interfaceId == type(IAccount).interfaceId ||      │
    │              interfaceId == type(IAccountExecute).interfaceId; │
    │      }                                                         │
    │  }                                                             │
    │                                                                │
    └────────────────────────────────────────────────────────────────┘
    • contracts/Nexus.sol

      // VULNERABILITY: Missing ERC-165 implementation required by ERC-7579
      contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgradeable {
          // Contract does not implement supportsInterface() function
          // No interface ID registration for ERC-165 standard
      }

2. Hook-Based Fee Bypass

Prerequisite Knowledge:

There are TWO different types of hooks in this vulnerability:

  1. ERC-7579 Account Hook: A module installed on the smart account itself. When the account executes any operation, the withHook modifier calls the hook’s preCheck() before and postCheck() after the execution. This is part of the ERC-7579 standard.

  2. Superform Protocol Hook: A separate hook system used by Superform for tracking operations. preExecute() records state before operation, postExecute() calculates results after. This is Superform’s own implementation.

The key insight: Account hooks run INSIDE the execution, while Superform hooks wrap the entire execution from outside.

Root Cause:

Superform measures account balance in postExecute() to calculate fees, assuming it accurately reflects the operation result. However, ERC-7579 account hooks execute between the actual operation and Superform’s postExecute(). A malicious account hook can transfer assets out before Superform measures the balance.

Attack Vector:

A user deploys a malicious ERC-7579 hook and installs it on their smart account. They grant the hook infinite approval for the target asset. When initiating a Superform vault redemption: (1) Superform’s preExecute records initial balance=0, (2) vault.redeem() transfers 1000 USDC to account, (3) ERC-7579 withHook triggers the malicious hook’s postCheck() which transfers 1000 USDC to user’s EOA, (4) Superform’s postExecute sees balance=0, calculates outAmount=0-0=0, charges zero fees. User receives 1000 USDC without paying fees.

Source:

  • [cantina/2025-05-superform-core] Outflow fees can be bypassed by leveraging ERC7579’s withHook modifier (MEDIUM-436.md)

    Business Scenario:

    Superform v2 - Cross-chain Yield Aggregation Protocol
    ┌────────────────────────────────────────────────────────────────┐
    │                                                                │
    │  What is Superform?                                            │
    │  A modular DeFi protocol for yield abstraction. Users can     │
    │  deposit/withdraw from vaults across multiple chains with     │
    │  a single signature. Uses ERC-7579 smart accounts.            │
    │                                                                │
    │  Key Components:                                               │
    │  • SuperExecutor: Executes hook chains on source chain        │
    │  • SuperDestinationExecutor: Executes on destination chain    │
    │  • SuperMerkleValidator: Validates user signatures            │
    │  • Superform Hooks: Protocol hooks for vault operations       │
    │    - Redeem4626VaultHook: Handles ERC-4626 vault redemptions  │
    │    - Tracks outAmount to calculate fees                        │
    │                                                                │
    │  Fee Model: Charge fees on outflows (redemptions from vaults) │
    │                                                                │
    └────────────────────────────────────────────────────────────────┘
    
    Two Types of Hooks (IMPORTANT!):
    ┌────────────────────────────────────────────────────────────────┐
    │                                                                │
    │  1. ERC-7579 Account Hook (installed on user's account)       │
    │  ┌──────────────────────────────────────────────────────────┐ │
    │  │  // In Nexus account (ERC-7579 implementation)           │ │
    │  │  modifier withHook(bytes calldata callData) {            │ │
    │  │      bytes memory hookData = _hook.preCheck(...);        │ │
    │  │      _;  // Execute the actual operation                 │ │
    │  │      _hook.postCheck(hookData);  // ← RUNS AFTER REDEEM! │ │
    │  │  }                                                       │ │
    │  │                                                          │ │
    │  │  function executeFromExecutor(...) withHook(callData) {  │ │
    │  │      // SuperExecutor calls this                         │ │
    │  │  }                                                       │ │
    │  └──────────────────────────────────────────────────────────┘ │
    │                                                                │
    │  2. Superform Protocol Hook (fee tracking, not ERC-7579)      │
    │  ┌──────────────────────────────────────────────────────────┐ │
    │  │  // Redeem4626VaultHook.sol                              │ │
    │  │  function _preExecute(...) {                             │ │
    │  │      outAmount = balanceOf(account);  // Record before   │ │
    │  │  }                                                       │ │
    │  │                                                          │ │
    │  │  function _postExecute(...) {                            │ │
    │  │      outAmount = balanceOf(account) - outAmount; // Diff │ │
    │  │  }                                                       │ │
    │  └──────────────────────────────────────────────────────────┘ │
    │                                                                │
    └────────────────────────────────────────────────────────────────┘
    
    Fee Calculation Flow (Normal):
    ┌────────────────────────────────────────────────────────────────┐
    │                                                                │
    │  Timeline:                                                     │
    │  ┌──────────────────────────────────────────────────────────┐ │
    │  │                                                          │ │
    │  │  T1: Superform preExecute()                              │ │
    │  │      outAmount = balanceOf(account) = 0                  │ │
    │  │                                                          │ │
    │  │  T2: vault.redeem() called                               │ │
    │  │      1000 USDC transferred to account                    │ │
    │  │                                                          │ │
    │  │  T3: Superform postExecute()                             │ │
    │  │      outAmount = balanceOf(account) - outAmount          │ │
    │  │               = 1000 - 0 = 1000                          │ │
    │  │      fee = 1000 * 1% = 10 USDC                           │ │
    │  │                                                          │ │
    │  └──────────────────────────────────────────────────────────┘ │
    │                                                                │
    └────────────────────────────────────────────────────────────────┘
    
    Attack Flow:
    ┌────────────────────────────────────────────────────────────────┐
    │                                                                │
    │  Attacker Goal: Redeem from vault without paying fees          │
    │                                                                │
    │  Step 1: Setup malicious hook on ERC-7579 account             │
    │  ┌──────────────────────────────────────────────────────────┐ │
    │  │  contract FeeBypassHook is IHook {                       │ │
    │  │      address owner;                                      │ │
    │  │      address asset;                                      │ │
    │  │                                                          │ │
    │  │      function postCheck(bytes calldata) external {       │ │
    │  │          // Transfer all assets to owner's EOA           │ │
    │  │          uint256 bal = IERC20(asset).balanceOf(msg.sender│ │
    │  │          IERC20(asset).transferFrom(msg.sender,          │ │
    │  │              owner, bal);                                │ │
    │  │      }                                                   │ │
    │  │  }                                                       │ │
    │  │                                                          │ │
    │  │  // Install hook on account                              │ │
    │  │  account.installModule(HOOK_TYPE, feeBypassHook, "");    │ │
    │  │                                                          │ │
    │  │  // Approve hook to transfer assets                      │ │
    │  │  account.execute(                                        │ │
    │  │      asset, 0,                                           │ │
    │  │      abi.encodeCall(IERC20.approve, (hook, MAX_UINT))    │ │
    │  │  );                                                      │ │
    │  └──────────────────────────────────────────────────────────┘ │
    │                                                                │
    │  Step 2: Execute redemption via Superform                     │
    │  ┌──────────────────────────────────────────────────────────┐ │
    │  │  Timeline:                                               │ │
    │  │                                                          │ │
    │  │  T1: Superform.preExecute()                              │ │
    │  │      → Records balance = 0                               │ │
    │  │                                                          │ │
    │  │  T2: Vault.redeem()                                      │ │
    │  │      → 1000 USDC transferred to account                  │ │
    │  │      → Account balance = 1000 USDC                       │ │
    │  │                                                          │ │
    │  │  T3: ERC-7579 withHook → hook.postCheck()                │ │
    │  │      → Hook transfers 1000 USDC to attacker's EOA        │ │
    │  │      → Account balance = 0 USDC                          │ │
    │  │                                                          │ │
    │  │  T4: Superform.postExecute()                             │ │
    │  │      → outAmount = 0 - 0 = 0                             │ │
    │  │      → fee = 0 * feeRate = 0                             │ │
    │  │      → NO FEE CHARGED!                                   │ │
    │  │                                                          │ │
    │  └──────────────────────────────────────────────────────────┘ │
    │                                                                │
    │  Result: Attacker receives 1000 USDC, pays 0 fees             │
    │                                                                │
    └────────────────────────────────────────────────────────────────┘
    
    Mitigation:
    ┌────────────────────────────────────────────────────────────────┐
    │                                                                │
    │  Options:                                                      │
    │  1. Track actual transfer amounts, not balance differences    │
    │  2. Use callback-based accounting that runs inside the hook   │
    │  3. Charge fees upfront based on expected amounts             │
    │  4. Whitelist allowed hooks for fee-sensitive operations      │
    │                                                                │
    └────────────────────────────────────────────────────────────────┘
    • src/core/hooks/vaults/4626/Redeem4626VaultHook.sol

      function _preExecute(address, address account, bytes calldata data) internal override {
          address yieldSource = data.extractYieldSource();
          asset = IERC4626(yieldSource).asset();
          outAmount = _getBalance(account, data); // Records initial balance
          usedShares = _getSharesBalance(account, data);
          spToken = yieldSource;
      }
      
      function _postExecute(address, address account, bytes calldata data) internal override {
          // VULNERABILITY: Balance may have been manipulated by account's hook
          outAmount = _getBalance(account, data) - outAmount;
          usedShares = usedShares - _getSharesBalance(account, data);
      }
      
      function _getBalance(address account, bytes memory) private view returns (uint256) {
          return IERC20(asset).balanceOf(account); // Direct balance check
      }

3. Infinite Validity Signature Rejection

Prerequisite Knowledge:

The validUntil parameter originates from ERC-4337 (Account Abstraction), not ERC-7579. In ERC-4337, the validateUserOp function returns a packed validationData containing validUntil and validAfter timestamps. The ERC-4337 specification states: “@param validUntil - Last timestamp this UserOperation is valid (or zero for infinite).” ERC-7579 Validator modules inherit this convention when implementing signature validation.

Root Cause:

The validator implementation directly compares validUntil >= block.timestamp without handling the special case where validUntil = 0 means infinite validity. Since block.timestamp is always positive (e.g., 1703318400), the condition 0 >= 1703318400 evaluates to false, causing signatures intended to never expire to be rejected immediately.

Attack Vector:

This is a denial of service vulnerability, not an attack. Any user attempting to use infinite validity signatures (validUntil=0) will have their operations rejected. This breaks legitimate use cases like session keys for dApps, automated trading bots, recurring payment authorizations, and cross-chain operations that need long-lived signatures.

Source:

  • [cantina/2025-05-superform-core] SuperValidatorBase incorrectly handles infinite validity signatures (validUntil=0) against ERC7579 specification (MEDIUM-417.md)

    Business Scenario:

    Superform v2 - Signature Validation for Cross-chain Operations
    ┌────────────────────────────────────────────────────────────────┐
    │                                                                │
    │  How Superform uses signatures:                                │
    │                                                                │
    │  1. User wants to do cross-chain yield operations             │
    │  2. Frontend builds a Merkle tree of all operations           │
    │  3. User signs the Merkle root ONCE                           │
    │  4. SuperBundler executes operations across chains            │
    │                                                                │
    │  Each Merkle leaf contains:                                    │
    │  ┌──────────────────────────────────────────────────────────┐ │
    │  │  struct SignatureData {                                  │ │
    │  │      uint48 validUntil;    // When signature expires     │ │
    │  │      bytes32 merkleRoot;   // Root of operation tree     │ │
    │  │      bytes32[] proofSrc;   // Proof for source chain     │ │
    │  │      bytes32[] proofDst;   // Proof for dest chain       │ │
    │  │      bytes signature;      // User's ECDSA signature     │ │
    │  │  }                                                       │ │
    │  └──────────────────────────────────────────────────────────┘ │
    │                                                                │
    └────────────────────────────────────────────────────────────────┘
    
    validUntil in ERC-4337 (Account Abstraction):
    ┌────────────────────────────────────────────────────────────────┐
    │                                                                │
    │  ERC-4337 validateUserOp returns packed validationData:        │
    │                                                                │
    │  uint256 validationData = (256 bits total)                     │
    │  ┌──────────────────────────────────────────────────────────┐ │
    │  │  Bits 0-159:   authorizer (address)                      │ │
    │  │                • 0 = valid signature                     │ │
    │  │                • 1 = invalid signature                   │ │
    │  │                                                          │ │
    │  │  Bits 160-207: validUntil (uint48)                       │ │
    │  │                • 0 = infinite (NEVER expires)            │ │
    │  │                • >0 = Unix timestamp of expiration       │ │
    │  │                                                          │ │
    │  │  Bits 208-255: validAfter (uint48)                       │ │
    │  │                • 0 = valid immediately                   │ │
    │  │                • >0 = Unix timestamp to start validity   │ │
    │  └──────────────────────────────────────────────────────────┘ │
    │                                                                │
    │  EntryPoint checks these values before executing UserOp.       │
    │                                                                │
    └────────────────────────────────────────────────────────────────┘
    
    The Bug:
    ┌────────────────────────────────────────────────────────────────┐
    │                                                                │
    │  SuperValidatorBase._isSignatureValid():                       │
    │                                                                │
    │  return signer == _accountOwners[sender]                       │
    │      && validUntil >= block.timestamp;                         │
    │                                                                │
    │  When validUntil = 0:                                          │
    │    0 >= block.timestamp (e.g., 1703318400)                     │
    │    0 >= 1703318400                                             │
    │    = false  ← WRONG! Should be true for infinite validity     │
    │                                                                │
    │  The code treats 0 as "already expired" instead of "never     │
    │  expires", breaking the ERC-4337 convention.                   │
    │                                                                │
    └────────────────────────────────────────────────────────────────┘
    
    Mitigation:
    ┌────────────────────────────────────────────────────────────────┐
    │                                                                │
    │  Handle the special case explicitly per ERC-4337 spec:         │
    │                                                                │
    │  function _isSignatureValid(...) returns (bool) {              │
    │      return signer == _accountOwners[sender] &&                │
    │             (validUntil == 0 || validUntil >= block.timestamp);│
    │  }                //    ↑                                      │
    │                   // 0 means infinite, skip timestamp check    │
    │                                                                │
    └────────────────────────────────────────────────────────────────┘
    • src/core/validators/SuperValidatorBase.sol

      function _isSignatureValid(address signer, address sender, uint48 validUntil)
          internal
          view
          virtual
          returns (bool)
      {
          /// @dev block.timestamp could vary between chains
          // VULNERABILITY: When validUntil=0 (infinite validity), this fails
          // because block.timestamp > 0, so 0 >= block.timestamp is false
          return signer == _accountOwners[sender] && validUntil >= block.timestamp;
      }

ERC-7579 Audit Checklist

High-Risk Scenarios: Modular account implementations, hook integrations, fee calculations, signature validation.

# Check Item Related Pattern
Does the account implement ERC-165 supportsInterface()? Required by ERC-7579 for ecosystem interoperability Pattern 1
Can user-installed hooks manipulate balances before protocol post-checks? Hooks run between pre and post execution Pattern 2
Does signature validation handle validUntil=0 as infinite validity? Per ERC-4337 spec, 0 means never expires Pattern 3
Are fee calculations based on balance differences? Can be bypassed by hooks transferring assets before measurement Pattern 2