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:
- Config: Handles configurations on the module, such as installation and uninstallation
- Business logic: Handles the core logic of the module, depending on the module type
- 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):
- Slot
A
whereA
is the address of the account (ie sender of theUserOperation
) - Slots of type
keccak256(A || X) + n
whereA
is the address of the account,||
means concatenation,X
is any value andn
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
orEXTCODE*
to an address that has no deployed code (excluding precompiles) - Module cannot perform a
*CALL
with value - Module cannot call the EntryPoint