ModuleKit
Build
Validators

Validators

Validators are modules that are called during the validation phase of a UserOperation. This means that their primary function is to verify the signature of a UserOperation and determine whether it is valid and should be executed. As a result, validators are the primary mechanism for enforcing access control on a smart account and are security critical.

A module can be broken down into three different domains:

  1. Config: Handles configurations on the module, such as installation and uninstallation
  2. Business logic: Handles the core logic of the module, depending on the module type
  3. Metadata: Additional data that is used to identify and correctly use a module

This page only covers domain 2 in the specific case of a Validator Module. For more information on domains 1 and 3, read through the Module page.

Building a validator

To build a compliant validator you need to ensure that it:

  • Inherits from the ERC7579ValidatorBase contract.
  • Implements the required functions of the interface.

An example of a compliant validator (without actual logic) looks like this:

contract ValidatorExample is ERC7579ValidatorBase {
    /*//////////////////////////////////////////////////////////////////////////
                                     CONFIG
    //////////////////////////////////////////////////////////////////////////*/
 
    ...
 
    /*//////////////////////////////////////////////////////////////////////////
                                     MODULE LOGIC
    //////////////////////////////////////////////////////////////////////////*/
 
    /**
     * Validates UserOperation
     * @param userOp PackedUserOperation to be validated.
     * @param userOpHash Hash of the UserOperation to be validated.
     * @return sigValidationResult the result of the signature validation, which can be:
     *  - 0 if the signature is valid
     *  - 1 if the signature is invalid
     *  - <20-byte> aggregatorOrSigFail, <6-byte> validUntil and <6-byte> validAfter (see ERC-4337
     * for more details)
     */
    function validateUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash
    )
        external
        view
        override
        returns (ValidationData)
    {
        return ValidationData.wrap(0);
    }
 
    /**
     * Validates an ERC-1271 signature
     * @param sender The sender of the ERC-1271 call to the account
     * @param hash The hash of the message
     * @param signature The signature of the message
     * @return sigValidationResult the result of the signature validation, which can be:
     *  - EIP1271_SUCCESS if the signature is valid
     *  - EIP1271_FAILED if the signature is invalid
     */
    function isValidSignatureWithSender(
        address sender,
        bytes32 hash,
        bytes calldata signature
    )
        external
        view
        virtual
        override
        returns (bytes4 sigValidationResult)
    {
        return EIP1271_FAILED;
    }
 
    /*//////////////////////////////////////////////////////////////////////////
                                     METADATA
    //////////////////////////////////////////////////////////////////////////*/
 
    ...
}

validateUserOp

This function is called by the account during the validation phase of ERC-4337. Its goal is to determine whether a PackedUserOperation should be executed. It returns a uint256 signalling whether to accept or reject a PackedUserOperation. The main way that this will happen in practice is that the userOp.signature will be recovered using the userOpHash and checked against some owner or set of owners stored on the module. The module may also return the variable VALIDATION_FAILED for added convenience.

Additionally, the ERC7579ValidatorBase includes a function _packValidationData(bool sigFailed, uint48 validUntil, uint48 validAfter), which packs the return data according to the ERC-4337 specifications on more complex return data. The schema of this return data is: <20-byte> aggregatorOrSigFail, <6-byte> validUntil and <6-byte> validAfter. This is to use either an aggregator or to ensure that a PackedUserOperation can only be executed before validUntil or after validAfter. To easily format the return data to use this schema, use _packValidationData which is prt of the ERC7579ValidatorBase.

Note that the in the ERC7579ValidatorBase interface, the return value of validateUserOp is not of type uint256 but of type ValidationData. This is a custom Solidity type for increased type safety. For the purposes of building a Module, it suffices to know that you can move between uint256 and ValidationData simply by calling either ValidationData.wrap(uint256) or ValidationData.unwrap(ValidationData). To learn more about custom types, read through this article (opens in a new tab).

isValidSignatureWithSender

This function is called by the account during an ERC-1271 (opens in a new tab) call on the account. Because this call is forwarded to the module, the module implements isValidSignatureWithSender as opposed to the ERC-1271 function isValidSignature, where the additional parameter is the msg.sender that called the account. The function should return EIP1271_SUCCESS if the signature is valid and EIP1271_FAILED otherwise, which will on most cases be determined using the signature and the hash.

Validator examples

ERC-4337 restrictions

ERC-4337 stipulates some restrictions for what can and cannot occur during the validation phase of a UserOperation. These restrictions are enforced by bundlers and aim to protect the alt mempool from Denial-of-Service attacks. These restrictions are laid out by ERC-7562 (opens in a new tab) and there is a summary of the rules that apply to modules below. Additionally, the ModuleKit allows you to check that your modules are compliant with these rules and you can find out more about this in the utilities section.

Storage accesses

A module may only use the following two patterns for storage access (read/write):

  1. Slot A where A is the address of the account (ie sender of the UserOperation)
  2. Slots of type keccak256(A || X) + n where A is the address of the account, || means concatenation, X is any value and n is an integer up to 128. More practically, this means that a module can use mappings (including nested mappings) but only if the final key that is used is the account address. Further, this mapping can point to a struct that can use up to 128 storage slots.

These same rules also apply to TSTORE and TLOAD.

Forbidden Opcodes

A module is not allowed to use the following opcodes:

  • BALANCE (0x31)
  • ORIGIN (0x32)
  • GASPRICE (0x3A)
  • BLOCKHASH (0x40)
  • COINBASE (0x41)
  • TIMESTAMP (0x42)
  • NUMBER (0x43)
  • PREVRANDAO/DIFFICULTY (0x44)
  • GASLIMIT (0x45)
  • SELFBALANCE (0x47)
  • BASEFEE (0x48)
  • GAS (0x5A)
  • CREATE (0xF0)
  • CREATE2 (0xF5)
  • INVALID (0xFE)
  • SELFDESTRUCT (0xFF)

One exception to this is that GAS may be used if immediately followed by a *CALL.

Further restrictions

Additionally, the following restrictions apply:

  • Module cannot *CALL or EXTCODE* to an address that has no deployed code (excluding precompiles)
  • Module cannot perform a *CALL with value
  • Module cannot call the EntryPoint