Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

- `PaymasterERC20Guarantor`: Add extension of `PaymasterERC20` that enables third parties to guarantee user operations by prefunding gas costs upfront, with repayment handling for successful operations.
- `ERC7579Validator`: Add abstract validator module for ERC-7579 accounts that provides base implementation for signature validation.
- `ERC7579SignatureValidator`: Add implementation of `ERC7579Validator` that enables ERC-7579 accounts to integrate with address-less cryptographic keys through ERC-7913 signature verification.
- `ERC7579Signature`: Add implementation of `ERC7579Validator` that enables ERC-7579 accounts to integrate with address-less cryptographic keys and account signatures through ERC-7913 signature verification.

## 29-04-2025

Expand Down
24 changes: 12 additions & 12 deletions contracts/account/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ This directory includes contracts to build accounts for ERC-4337. These include:
* {AccountERC7579}: An extension of `Account` that implements support for ERC-7579 modules.
* {AccountERC7579Hooked}: An extension of `AccountERC7579` with support for a single hook module (type 4).
* {ERC7821}: Minimal batch executor implementation contracts. Useful to enable easy batch execution for smart contracts.
* {ERC7579Validator}: Abstract validator module for ERC-7579 accounts that provides base implementation for signature validation.
* {ERC7579SignatureValidator}: Implementation of ERC7579Validator using ERC-7913 signature verification for address-less cryptographic keys.
* {ERC7579Multisig}: An abstract multisig module for ERC-7579 accounts using ERC-7913 signer keys.
* {ERC7579MultisigWeighted}: An abstract weighted multisig module that allows different weights to be assigned to signers.
* {ERC7579MultisigConfirmation}: An abstract confirmation-based multisig module that each signer to provide a confirmation signature.
* {ERC7579Executor}: An executor module that enables executing calls from accounts where the it's installed.
* {ERC7579DelayedExecutor}: An executor module that adds a delay before executing an account operation.
* {ERC7579Validator}: Abstract validator module for ERC-7579 accounts that provides base implementation for signature validation.
* {ERC7579Signature}: Implementation of {ERC7579Validator} using ERC-7913 signature verification for address-less cryptographic keys and account signatures.
* {ERC7579Multisig}: An extension of {ERC7579Validator} that enables validation using ERC-7913 signer keys.
* {ERC7579MultisigWeighted}: An extension of {ERC7579Multisig} that allows different weights to be assigned to signers.
* {ERC7579MultisigConfirmation}: An extension of {ERC7579Multisig} that requires each signer to provide a confirmation signature.
* {PaymasterCore}: An ERC-4337 paymaster implementation that includes the core logic to validate and pay for user operations.
* {PaymasterERC20}: A paymaster that allows users to pay for user operations using ERC-20 tokens.
* {PaymasterERC20Guarantor}: A paymaster that enables third parties to guarantee user operations by pre-funding gas costs, with the option for users to repay or for guarantors to absorb the cost.
Expand All @@ -35,12 +35,6 @@ This directory includes contracts to build accounts for ERC-4337. These include:

== Modules

{{ERC7579Multisig}}

{{ERC7579MultisigWeighted}}

{{ERC7579MultisigConfirmation}}

=== Executors

{{ERC7579Executor}}
Expand All @@ -51,7 +45,13 @@ This directory includes contracts to build accounts for ERC-4337. These include:

{{ERC7579Validator}}

{{ERC7579SignatureValidator}}
{{ERC7579Signature}}

{{ERC7579Multisig}}

{{ERC7579MultisigWeighted}}

{{ERC7579MultisigConfirmation}}

== Paymaster

Expand Down
29 changes: 6 additions & 23 deletions contracts/account/modules/ERC7579Multisig.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,23 @@ pragma solidity ^0.8.27;

import {ERC7913Utils} from "../../utils/cryptography/ERC7913Utils.sol";
import {EnumerableSetExtended} from "../../utils/structs/EnumerableSetExtended.sol";
import {IERC7579Module} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
import {Mode} from "@openzeppelin/contracts/account/utils/draft-ERC7579Utils.sol";
import {ERC7579Validator} from "./ERC7579Validator.sol";

/**
* @dev Implementation of an {IERC7579Module} that uses ERC-7913 signers for multisignature
* @dev Implementation of an {ERC7579Validator} that uses ERC-7913 signers for multisignature
* validation.
*
* This module provides a base implementation for multisignature validation that can be
* attached to any function through the {_validateMultisignature} internal function. The signers
* attached to any function through the {_rawERC7579Validation} internal function. The signers
* are represented using the ERC-7913 format, which concatenates a verifier address and
* a key: `verifier || key`.
*
* Example implementation:
*
* ```solidity
* function execute(
* address account,
* Mode mode,
* bytes calldata executionCalldata,
* bytes32 salt,
* bytes calldata signature
* ) public virtual {
* require(_validateMultisignature(account, hash, signature));
* // ... rest of execute logic
* }
* ```
*
* Example use case:
*
* A smart account with this module installed can require multiple signers to approve
* operations before they are executed, such as requiring 3-of-5 guardians to approve
* a social recovery operation.
*/
abstract contract ERC7579Multisig is IERC7579Module {
abstract contract ERC7579Multisig is ERC7579Validator {
using EnumerableSetExtended for EnumerableSetExtended.BytesSet;
using ERC7913Utils for bytes32;
using ERC7913Utils for bytes;
Expand Down Expand Up @@ -178,11 +161,11 @@ abstract contract ERC7579Multisig is IERC7579Module {
* Where `signingSigners` are the authorized signers and signatures are their corresponding
* signatures of the operation `hash`.
*/
function _validateMultisignature(
function _rawERC7579Validation(
address account,
bytes32 hash,
bytes calldata signature
) internal view virtual returns (bool) {
) internal view virtual override returns (bool) {
(bytes[] memory signingSigners, bytes[] memory signatures) = abi.decode(signature, (bytes[], bytes[]));
return
_validateThreshold(account, signingSigners) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,53 +10,22 @@ import {IERC7579Module} from "@openzeppelin/contracts/interfaces/draft-IERC7579.
* @dev Implementation of {ERC7579Validator} module using ERC-7913 signature verification.
*
* This validator allows ERC-7579 accounts to integrate with address-less cryptographic keys
* through the ERC-7913 signature verification system. Each account can store its own ERC-7913
* formatted signer (a concatenation of a verifier address and a key: `verifier || key`).
* and account signatures through the ERC-7913 signature verification system. Each account
* can store its own ERC-7913 formatted signer (a concatenation of a verifier address and a
* key: `verifier || key`).
*
* This enables accounts to use signature schemes without requiring each key to have its own
* Ethereum address.
*
* The validator implements two key functions from ERC-7579:
*
* * `validateUserOp`: Validates ERC-4337 user operations using ERC-7913 signatures
* * `isValidSignatureWithSender`: Implements ERC-1271 signature verification via ERC-7913
*
* Example usage with an account:
*
* ```solidity
* contract MyAccount is Account, AccountERC7579 {
* function initialize(address validator, bytes memory signerData) public initializer {
* // Install the validator module
* bytes memory initData = abi.encode(signerData);
* _installModule(MODULE_TYPE_VALIDATOR, validator, initData);
* }
* }
* ```
*
* Example of validator installation with a P256 key:
*
* ```solidity
* // Address of the P256 verifier contract
* address p256verifier = 0x123...;
*
* // P256 public key bytes
* bytes memory p256PublicKey = 0x456...;
*
* // Combine into ERC-7913 signer format
* bytes memory signerData = bytes.concat(abi.encodePacked(p256verifier), p256PublicKey);
*
* // Initialize the account with the validator and signer
* account.initialize(address(new ERC7579SignatureValidator()), signerData);
* ```
* Ethereum address.A smart account with this module installed can keep an emergency key as a
* backup.
*/
contract ERC7579SignatureValidator is ERC7579Validator {
contract ERC7579Signature is ERC7579Validator {
mapping(address account => bytes signer) private _signers;

/// @dev Emitted when the signer is set.
event ERC7579SignatureValidatorSignerSet(address indexed account, bytes signer);
event ERC7579SignatureSignerSet(address indexed account, bytes signer);

/// @dev Thrown when the signer length is less than 20 bytes.
error ERC7579SignatureValidatorInvalidSignerLength();
error ERC7579SignatureInvalidSignerLength();

/// @dev Return the ERC-7913 signer (i.e. `verifier || key`).
function signer(address account) public view virtual returns (bytes memory) {
Expand Down Expand Up @@ -88,30 +57,30 @@ contract ERC7579SignatureValidator is ERC7579Validator {

/// @dev Sets the ERC-7913 signer (i.e. `verifier || key`) for the calling account.
function setSigner(bytes memory signer_) public virtual {
require(signer_.length >= 20, ERC7579SignatureValidatorInvalidSignerLength());
require(signer_.length >= 20, ERC7579SignatureInvalidSignerLength());
_setSigner(msg.sender, signer_);
}

/// @dev Internal version of {setSigner} that takes an `account` as argument without validating `signer_`.
function _setSigner(address account, bytes memory signer_) internal virtual {
_signers[account] = signer_;
emit ERC7579SignatureValidatorSignerSet(account, signer_);
emit ERC7579SignatureSignerSet(account, signer_);
}

/**
* @dev See {ERC7579Validator-_rawSignatureValidationWithSender}.
* @dev See {ERC7579Validator-_rawERC7579Validation}.
*
* Validates a `signature` using ERC-7913 verification.
*
* This base implementation ignores the `sender` parameter and validates using
* the account's stored signer. Derived contracts can override this to implement
* custom validation logic based on the sender.
*/
function _rawSignatureValidationWithSender(
address /* sender */,
function _rawERC7579Validation(
address account,
bytes32 hash,
bytes calldata signature
) internal view virtual override returns (bool) {
return ERC7913Utils.isValidSignatureNow(signer(msg.sender), hash, signature);
return ERC7913Utils.isValidSignatureNow(signer(account), hash, signature);
}
}
49 changes: 35 additions & 14 deletions contracts/account/modules/ERC7579Validator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import {IERC7579Module, IERC7579Validator, MODULE_TYPE_VALIDATOR} from "@openzep
import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";

/**
* @dev Abstract validator module for ERC-7579 accounts.
*
* This contract provides the base implementation for signature validation in ERC-7579 accounts.
* Developers must implement the onInstall, onUninstall, and {_rawSignatureValidationWithSender}
* Developers must implement the onInstall, onUninstall, and {_rawERC7579Validation}
* functions in derived contracts to define the specific signature validation logic.
*
* Example usage:
Expand All @@ -25,15 +26,31 @@ import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
* // Uninstall logic here
* }
*
* function _rawSignatureValidationWithSender(
* address sender,
* function _rawERC7579Validation(
* address account,
* bytes32 hash,
* bytes calldata signature
* ) internal view override returns (bool) {
* // Signature validation logic here
* }
* }
* ```
*
* Developers can restrict other operations by using the internal {_rawERC7579Validation}.
* Example usage:
*
* ```solidity
* function execute(
* address account,
* Mode mode,
* bytes calldata executionCalldata,
* bytes32 salt,
* bytes calldata signature
* ) public virtual {
* require(_rawERC7579Validation(account, hash, signature));
* // ... rest of execute logic
* }
* ```
*/
abstract contract ERC7579Validator is IERC7579Module, IERC7579Validator {
/// @inheritdoc IERC7579Module
Expand All @@ -47,33 +64,37 @@ abstract contract ERC7579Validator is IERC7579Module, IERC7579Validator {
bytes32 userOpHash
) public view virtual returns (uint256) {
return
_rawSignatureValidationWithSender(msg.sender, userOpHash, userOp.signature)
_rawERC7579Validation(msg.sender, userOpHash, userOp.signature)
? ERC4337Utils.SIG_VALIDATION_SUCCESS
: ERC4337Utils.SIG_VALIDATION_FAILED;
}

/// @inheritdoc IERC7579Validator
/**
* @dev See {IERC7579Validator-isValidSignatureWithSender}.
*
* Ignores the `sender` parameter and validates using {_rawERC7579Validation}.
* Consider overriding this function to implement custom validation logic
* based on the original sender.
*/
function isValidSignatureWithSender(
address sender,
address /* sender */,
bytes32 hash,
bytes calldata signature
) public view virtual returns (bytes4) {
return
_rawSignatureValidationWithSender(sender, hash, signature)
_rawERC7579Validation(msg.sender, hash, signature)
? IERC1271.isValidSignature.selector
: bytes4(0xffffffff);
}

/**
* @dev Internal version of {isValidSignatureWithSender} to be implemented by derived contracts.
* @dev Validation algorithm.
*
* WARNING: Signature validation is a critical security function for smart accounts as it
* determines whether operations can be executed on the account. Implementations must carefully
* handle cryptographic verification to prevent unauthorized access. Thorough security review and
* testing are required before deployment.
* WARNING: Validation is a critical security function. Implementations must carefully
* handle cryptographic verification to prevent unauthorized access.
*/
function _rawSignatureValidationWithSender(
address sender,
function _rawERC7579Validation(
address account,
bytes32 hash,
bytes calldata signature
) internal view virtual returns (bool);
Expand Down
19 changes: 16 additions & 3 deletions contracts/mocks/account/modules/ERC7579MultisigMocks.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.27;

import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ERC7579Executor} from "../../../account/modules/ERC7579Executor.sol";
import {ERC7579Validator} from "../../../account/modules/ERC7579Validator.sol";
import {ERC7579Multisig} from "../../../account/modules/ERC7579Multisig.sol";
import {ERC7579MultisigWeighted} from "../../../account/modules/ERC7579MultisigWeighted.sol";
import {ERC7579MultisigConfirmation} from "../../../account/modules/ERC7579MultisigConfirmation.sol";
Expand All @@ -14,6 +15,10 @@ abstract contract ERC7579MultisigExecutorMock is EIP712, ERC7579Executor, ERC757
bytes32 private constant EXECUTE_OPERATION =
keccak256("ExecuteOperation(address account,bytes32 mode,bytes executionCalldata,bytes32 salt)");

function isModuleType(uint256 moduleTypeId) public pure override(ERC7579Executor, ERC7579Validator) returns (bool) {
return ERC7579Executor.isModuleType(moduleTypeId) || ERC7579Executor.isModuleType(moduleTypeId);
}

// Data encoding: [uint16(executionCalldataLength), executionCalldata, signature]
function _validateExecution(
address account,
Expand All @@ -24,7 +29,7 @@ abstract contract ERC7579MultisigExecutorMock is EIP712, ERC7579Executor, ERC757
uint16 executionCalldataLength = uint16(uint256(bytes32(data[0:2]))); // First 2 bytes are the length
bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
bytes32 typeHash = _getExecuteTypeHash(account, salt, mode, executionCalldata);
require(_validateMultisignature(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
require(_rawERC7579Validation(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
return executionCalldata;
}

Expand All @@ -42,6 +47,10 @@ abstract contract ERC7579MultisigWeightedExecutorMock is EIP712, ERC7579Executor
bytes32 private constant EXECUTE_OPERATION =
keccak256("ExecuteOperation(address account,bytes32 mode,bytes executionCalldata,bytes32 salt)");

function isModuleType(uint256 moduleTypeId) public pure override(ERC7579Executor, ERC7579Validator) returns (bool) {
return ERC7579Executor.isModuleType(moduleTypeId) || ERC7579Executor.isModuleType(moduleTypeId);
}

// Data encoding: [uint16(executionCalldataLength), executionCalldata, signature]
function _validateExecution(
address account,
Expand All @@ -52,7 +61,7 @@ abstract contract ERC7579MultisigWeightedExecutorMock is EIP712, ERC7579Executor
uint16 executionCalldataLength = uint16(uint256(bytes32(data[0:2]))); // First 2 bytes are the length
bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
bytes32 typeHash = _getExecuteTypeHash(account, salt, mode, executionCalldata);
require(_validateMultisignature(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
require(_rawERC7579Validation(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
return executionCalldata;
}

Expand All @@ -70,6 +79,10 @@ abstract contract ERC7579MultisigConfirmationExecutorMock is ERC7579Executor, ER
bytes32 private constant EXECUTE_OPERATION =
keccak256("ExecuteOperation(address account,bytes32 mode,bytes executionCalldata,bytes32 salt)");

function isModuleType(uint256 moduleTypeId) public pure override(ERC7579Executor, ERC7579Validator) returns (bool) {
return ERC7579Executor.isModuleType(moduleTypeId) || ERC7579Executor.isModuleType(moduleTypeId);
}

// Data encoding: [uint16(executionCalldataLength), executionCalldata, signature]
function _validateExecution(
address account,
Expand All @@ -80,7 +93,7 @@ abstract contract ERC7579MultisigConfirmationExecutorMock is ERC7579Executor, ER
uint16 executionCalldataLength = uint16(uint256(bytes32(data[0:2]))); // First 2 bytes are the length
bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
bytes32 typeHash = _getExecuteTypeHash(account, salt, mode, executionCalldata);
require(_validateMultisignature(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
require(_rawERC7579Validation(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
return executionCalldata;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async function fixture() {
await setBalance(eoa.address, ethers.WeiPerEther);

// ERC-7579 validator module
const validator = await ethers.deployContract('$ERC7579SignatureValidator');
const validator = await ethers.deployContract('$ERC7579Signature');

// ERC-4337 account
const helper = new ERC4337Helper();
Expand Down
2 changes: 1 addition & 1 deletion test/account/extensions/AccountERC7579.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ async function fixture() {
const anotherTarget = await ethers.deployContract('CallReceiverMockExtended');

// ERC-7579 signature validator
const erc7579Validator = await ethers.deployContract('$ERC7579SignatureValidator');
const erc7579Validator = await ethers.deployContract('$ERC7579Signature');

// ERC-7913 verifiers
const verifierP256 = await ethers.deployContract('ERC7913P256Verifier');
Expand Down
Loading