Skip to content
Open
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
22 changes: 11 additions & 11 deletions gas-snapshots/ModularAccount.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"Runtime_AccountCreation": "176053",
"Runtime_BatchTransfers": "92180",
"Runtime_Erc20Transfer": "77942",
"Runtime_InstallSessionKey_Case1": "429401",
"Runtime_NativeTransfer": "54261",
"Runtime_UseSessionKey_Case1_Counter": "78463",
"Runtime_UseSessionKey_Case1_Token": "111478",
"Runtime_Erc20Transfer": "77964",
"Runtime_InstallSessionKey_Case1": "429379",
"Runtime_NativeTransfer": "54283",
"Runtime_UseSessionKey_Case1_Counter": "78485",
"Runtime_UseSessionKey_Case1_Token": "111500",
"UserOp_BatchTransfers": "178933",
"UserOp_Erc20Transfer": "165901",
"UserOp_InstallSessionKey_Case1": "518400",
"UserOp_NativeTransfer": "142174",
"UserOp_UseSessionKey_Case1_Counter": "175309",
"UserOp_UseSessionKey_Case1_Token": "208402",
"UserOp_deferredValidation": "214001"
"UserOp_Erc20Transfer": "165923",
"UserOp_InstallSessionKey_Case1": "518378",
"UserOp_NativeTransfer": "142196",
"UserOp_UseSessionKey_Case1_Counter": "175331",
"UserOp_UseSessionKey_Case1_Token": "208424",
"UserOp_deferredValidation": "213793"
}
26 changes: 13 additions & 13 deletions gas-snapshots/SemiModularAccount.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"Runtime_AccountCreation": "97770",
"Runtime_BatchTransfers": "88037",
"Runtime_Erc20Transfer": "73844",
"Runtime_InstallSessionKey_Case1": "427632",
"Runtime_NativeTransfer": "50173",
"Runtime_UseSessionKey_Case1_Counter": "78765",
"Runtime_UseSessionKey_Case1_Token": "111780",
"UserOp_BatchTransfers": "174087",
"UserOp_Erc20Transfer": "161123",
"UserOp_InstallSessionKey_Case1": "515729",
"UserOp_NativeTransfer": "137402",
"UserOp_UseSessionKey_Case1_Counter": "175540",
"UserOp_UseSessionKey_Case1_Token": "208657",
"UserOp_deferredValidation": "210031"
"Runtime_BatchTransfers": "88048",
"Runtime_Erc20Transfer": "73877",
"Runtime_InstallSessionKey_Case1": "427621",
"Runtime_NativeTransfer": "50206",
"Runtime_UseSessionKey_Case1_Counter": "78787",
"Runtime_UseSessionKey_Case1_Token": "111802",
"UserOp_BatchTransfers": "174076",
"UserOp_Erc20Transfer": "161134",
"UserOp_InstallSessionKey_Case1": "515696",
"UserOp_NativeTransfer": "137413",
"UserOp_UseSessionKey_Case1_Counter": "175562",
"UserOp_UseSessionKey_Case1_Token": "208679",
"UserOp_deferredValidation": "210190"
}
8 changes: 2 additions & 6 deletions gas/modular-account/ModularAccount.gas.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -269,14 +269,10 @@ contract ModularAccountGasTest is ModularAccountBenchmarkBase("ModularAccount")
uint48 deferredInstallDeadline = 0;

bytes32 digest = _getDeferredInstallStruct(
account1, userOp.nonce, deferredInstallDeadline, deferredValidationInstallCall
account1, userOp.nonce, signerValidation, deferredInstallDeadline, deferredValidationInstallCall
);

bytes memory deferredValidationSig = _signRawHash(
vm,
owner1Key,
_getModuleReplaySafeHash(address(account1), address(singleSignerValidationModule), digest)
);
bytes memory deferredValidationSig = _signRawHash(vm, owner1Key, digest);

userOp.signature = _encodeDeferredInstallUOSignature(
_packDeferredInstallData(
Expand Down
2 changes: 1 addition & 1 deletion gas/modular-account/SemiModularAccount.gas.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ contract ModularAccountGasTest is ModularAccountBenchmarkBase("SemiModularAccoun
uint48 deferredInstallDeadline = 0;

bytes32 digest = _getDeferredInstallStruct(
account1, userOp.nonce, deferredInstallDeadline, deferredValidationInstallCall
account1, userOp.nonce, signerValidation, deferredInstallDeadline, deferredValidationInstallCall
);

bytes memory deferredValidationSig = _signRawHash(vm, owner1Key, digest);
Expand Down
71 changes: 58 additions & 13 deletions src/account/ModularAccountBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,13 @@ abstract contract ModularAccountBase is
EITHER
}

// keccak256("EIP712Domain(uint256 chainId,address verifyingContract)")
// keccak256("EIP712Domain(uint256 chainId,address verifyingContract,bytes32 salt)")
bytes32 internal constant _DOMAIN_SEPARATOR_TYPEHASH =
0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218;
0x71062c282d40422f744945d587dbf4ecfd4f9cfad1d35d62c944373009d96162;

// keccak256("ReplaySafeHash(bytes32 hash)")
bytes32 private constant _REPLAY_SAFE_HASH_TYPEHASH =
0x294a8735843d4afb4f017c76faf3b7731def145ed0025fc9b1d5ce30adf113ff;

// keccak256("DeferredAction(uint256 nonce,uint48 deadline,bytes call)")
bytes32 internal constant _DEFERRED_ACTION_TYPEHASH =
Expand Down Expand Up @@ -358,7 +362,7 @@ abstract contract ModularAccountBase is
(ValidationLocator locator, bytes calldata signatureRemainder) =
ValidationLocatorLib.loadFromSignature(signature);

return _isValidSignature(locator.lookupKey(), hash, signatureRemainder);
return _isValidSignature(locator, hash, signatureRemainder);
}

/// @inheritdoc IERC165
Expand Down Expand Up @@ -396,6 +400,20 @@ abstract contract ModularAccountBase is
super.upgradeToAndCall(newImplementation, data);
}

/// @notice Returns the replay-safe hash generated from the passed typed data hash for 1271 validation.
/// @param hash The typed data hash to wrap in a replay-safe hash.
/// @return The replay-safe hash, to be used for 1271 signature generation.
///
/// @dev Generates a replay-safe hash to wrap a standard typed data hash. This prevents replay attacks by
/// enforcing the domain separator, which includes this contract's address, the chainId, and the validation
/// module & entity id. This is only relevant for 1271 validation because UserOp validation relies on the UO
/// hash and the Entrypoint has safeguards.
function replaySafeHash(bytes32 hash, ModuleEntity validationModuleEntity) public view returns (bytes32) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making this public will increase bytecode size, and isn't strictly necessary (plus, doing an eth_call during txn prep is usually slow and requires implementation knowledge anyways.

But, this would make it easier to debug.

return MessageHashUtils.toTypedDataHash({
domainSeparator: _domainSeparator(validationModuleEntity), structHash: _hashStructReplaySafeHash(hash)
});
}

// INTERNAL FUNCTIONS

// Parent function validateUserOp enforces that this call can only be made by the EntryPoint
Expand Down Expand Up @@ -503,7 +521,12 @@ abstract contract ModularAccountBase is

uint48 deadline = uint48(bytes6(encodedData[21:27]));

bytes32 typedDataHash = _computeDeferredActionHash(userOpNonce, deadline, encodedData[27:]);
bytes32 typedDataHash = _computeDeferredActionHash(
userOpNonce,
defActionValidationLocator.lookupKey().moduleEntity(_validationStorage),
deadline,
encodedData[27:]
);

// Check if the outer validation applies to the function call
_checkIfValidationAppliesCallData(
Expand Down Expand Up @@ -734,11 +757,12 @@ abstract contract ModularAccountBase is
ExecutionLib.invokeRuntimeCallBufferValidation(callBuffer, runtimeValidationFunction, authorization);
}

function _computeDeferredActionHash(uint256 userOpNonce, uint48 deadline, bytes calldata selfCall)
internal
view
returns (bytes32)
{
function _computeDeferredActionHash(
uint256 userOpNonce,
ModuleEntity validationModuleEntity,
uint48 deadline,
bytes calldata selfCall
) internal view returns (bytes32) {
// Note:
// - A zero deadline translates to "no deadline"
// - The user op nonce also includes the data for:
Expand Down Expand Up @@ -781,7 +805,8 @@ abstract contract ModularAccountBase is
structHash := keccak256(fmp, 0x80)
}

bytes32 typedDataHash = MessageHashUtils.toTypedDataHash(_domainSeparator(), structHash);
bytes32 typedDataHash =
MessageHashUtils.toTypedDataHash(_domainSeparator(validationModuleEntity), structHash);

return typedDataHash;
}
Expand All @@ -805,15 +830,21 @@ abstract contract ModularAccountBase is
}
}

function _isValidSignature(ValidationLookupKey validationLookupKey, bytes32 hash, bytes calldata signature)
function _isValidSignature(ValidationLocator validationLocator, bytes32 hash, bytes calldata signature)
internal
view
returns (bytes4)
{
ValidationLookupKey validationLookupKey = validationLocator.lookupKey();
ValidationStorage storage _validationStorage = getAccountStorage().validationStorage[validationLookupKey];

HookConfig[] memory preSignatureValidationHooks = MemManagementLib.loadValidationHooks(_validationStorage);

if (!validationLocator.isSkipReplayProtection()) {
ModuleEntity validationModuleEntity = validationLookupKey.moduleEntity(_validationStorage);
hash = replaySafeHash(hash, validationModuleEntity);
}

SigCallBuffer sigCallBuffer;
if (!_validationIsNative(validationLookupKey) || preSignatureValidationHooks.length > 0) {
sigCallBuffer = ExecutionLib.allocateSigCallBuffer(hash, signature);
Expand Down Expand Up @@ -1104,7 +1135,7 @@ abstract contract ModularAccountBase is
return getAccountStorage().validationStorage[validationFunction].selectors.contains(toSetValue(selector));
}

function _domainSeparator() internal view returns (bytes32) {
function _domainSeparator(ModuleEntity validationModuleEntity) internal view returns (bytes32) {
bytes32 result;

// Compute the hash without permanently allocating memory
Expand All @@ -1113,12 +1144,26 @@ abstract contract ModularAccountBase is
mstore(fmp, _DOMAIN_SEPARATOR_TYPEHASH)
mstore(add(fmp, 0x20), chainid())
mstore(add(fmp, 0x40), address())
result := keccak256(fmp, 0x60)
mstore(add(fmp, 0x60), validationModuleEntity)
result := keccak256(fmp, 0x80)
}

return result;
}

/// @notice Adds a EIP-712 replay safe hash wrapper to the digest
/// @param hash The hash to wrap in a replay-safe hash
/// @return The replay-safe hash
function _hashStructReplaySafeHash(bytes32 hash) internal pure virtual returns (bytes32) {
bytes32 res;
assembly ("memory-safe") {
mstore(0x00, _REPLAY_SAFE_HASH_TYPEHASH)
mstore(0x20, hash)
res := keccak256(0, 0x40)
}
return res;
}

// A virtual function to detect if a validation function is natively implemented. Used for determining call
// buffer allocation.
function _validationIsNative(ValidationLookupKey) internal pure virtual returns (bool) {
Expand Down
40 changes: 0 additions & 40 deletions src/account/SemiModularAccountBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,6 @@ abstract contract SemiModularAccountBase is ModularAccountBase {
bool fallbackSignerDisabled;
}

// keccak256("ReplaySafeHash(bytes32 hash)")
bytes32 private constant _REPLAY_SAFE_HASH_TYPEHASH =
0x294a8735843d4afb4f017c76faf3b7731def145ed0025fc9b1d5ce30adf113ff;

// keccak256("ERC6900.SemiModularAccount.Storage")
uint256 internal constant _SEMI_MODULAR_ACCOUNT_STORAGE_SLOT =
0x5b9dc9aa943f8fa2653ceceda5e3798f0686455280432166ba472eca0bc17a32;
Expand Down Expand Up @@ -151,12 +147,6 @@ abstract contract SemiModularAccountBase is ModularAccountBase {
if (validationLookupKey.eq(FALLBACK_VALIDATION_LOOKUP_KEY)) {
address fallbackSigner = _getFallbackSigner();

// If called during validateUserOp, this implies that we're doing a deferred validation installation.
// In this case, as the hash is already replay-safe, we don't need to wrap it.
if (msg.sig != this.validateUserOp.selector) {
hash = _replaySafeHash(hash);
}

if (_checkSignature(fallbackSigner, hash, signature)) {
return _1271_MAGIC_VALUE;
}
Expand Down Expand Up @@ -233,23 +223,6 @@ abstract contract SemiModularAccountBase is ModularAccountBase {
return _storage.fallbackSigner;
}

/// @notice Returns the replay-safe hash generated from the passed typed data hash for 1271 validation.
/// @param hash The typed data hash to wrap in a replay-safe hash.
/// @return The replay-safe hash, to be used for 1271 signature generation.
///
/// @dev Generates a replay-safe hash to wrap a standard typed data hash. This prevents replay attacks by
/// enforcing the domain separator, which includes this contract's address and the chainId. This is only
/// relevant for 1271 validation because UserOp validation relies on the UO hash and the Entrypoint has
/// safeguards.
///
/// NOTE: Like in signature-based validation modules, the returned hash should be used to generate signatures,
/// but the original hash should be passed to the external-facing function for 1271 validation.
function _replaySafeHash(bytes32 hash) internal view returns (bytes32) {
return MessageHashUtils.toTypedDataHash({
domainSeparator: _domainSeparator(), structHash: _hashStructReplaySafeHash(hash)
});
}

function _getSemiModularAccountStorage() internal pure returns (SemiModularAccountStorage storage) {
SemiModularAccountStorage storage _storage;
assembly ("memory-safe") {
Expand All @@ -269,19 +242,6 @@ abstract contract SemiModularAccountBase is ModularAccountBase {
return validationLookupKey.eq(FALLBACK_VALIDATION_LOOKUP_KEY);
}

/// @notice Adds a EIP-712 replay safe hash wrapper to the digest
/// @param hash The hash to wrap in a replay-safe hash
/// @return The replay-safe hash
function _hashStructReplaySafeHash(bytes32 hash) internal pure virtual returns (bytes32) {
bytes32 res;
assembly ("memory-safe") {
mstore(0x00, _REPLAY_SAFE_HASH_TYPEHASH)
mstore(0x20, hash)
res := keccak256(0, 0x40)
}
return res;
}

/// @dev Overrides ModularAccountView.
function _isNativeFunction(uint32 selector) internal pure virtual override returns (bool) {
return super._isNativeFunction(selector) || selector == uint32(this.updateFallbackSignerData.selector)
Expand Down
10 changes: 10 additions & 0 deletions src/libraries/ValidationLocatorLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ library ValidationLocatorLib {
uint8 internal constant _VALIDATION_TYPE_GLOBAL = 1;
uint8 internal constant _HAS_DEFERRED_ACTION = 2;
uint8 internal constant _IS_DIRECT_CALL_VALIDATION = 4;
uint8 internal constant _IS_SKIP_REPLAY_PROTECTION = 8;

function moduleEntity(ValidationLookupKey _lookupKey, ValidationStorage storage validationStorage)
internal
Expand Down Expand Up @@ -198,6 +199,10 @@ library ValidationLocatorLib {
return (ValidationLocator.unwrap(locator) & _HAS_DEFERRED_ACTION) != 0;
}

function isSkipReplayProtection(ValidationLocator locator) internal pure returns (bool) {
return (ValidationLocator.unwrap(locator) & _IS_SKIP_REPLAY_PROTECTION) != 0;
}

function isDirectCallValidation(ValidationLookupKey _lookupKey) internal pure returns (bool) {
return (ValidationLookupKey.unwrap(_lookupKey) & _IS_DIRECT_CALL_VALIDATION) != 0;
}
Expand Down Expand Up @@ -318,6 +323,11 @@ library ValidationLocatorLib {
return bytes.concat(abi.encodePacked(options, uint32(validationEntityId)), signature);
}

function setSkipReplayProtection(bytes memory signature) internal pure returns (bytes memory result) {
signature[0] = bytes1(uint8(signature[0]) | _IS_SKIP_REPLAY_PROTECTION);
return signature;
}

function packSignatureDirectCall(
address directCallValidation,
bool _isGlobal,
Expand Down
6 changes: 2 additions & 4 deletions src/modules/validation/SingleSignerValidationModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ pragma solidity ^0.8.28;

import {IModule} from "@erc6900/reference-implementation/interfaces/IModule.sol";
import {IValidationModule} from "@erc6900/reference-implementation/interfaces/IValidationModule.sol";
import {ReplaySafeWrapper} from "@erc6900/reference-implementation/modules/ReplaySafeWrapper.sol";
import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
Expand All @@ -41,7 +40,7 @@ import {ModuleBase} from "../ModuleBase.sol";
/// - This validation supports ERC-1271. The signature is valid if it is signed by the owner's private key.
/// - This validation supports composition that other validation can relay on entities in this validation to
/// validate partially or fully.
contract SingleSignerValidationModule is IValidationModule, ReplaySafeWrapper, ModuleBase {
contract SingleSignerValidationModule is IValidationModule, ModuleBase {
uint256 internal constant _SIG_VALIDATION_PASSED = 0;
uint256 internal constant _SIG_VALIDATION_FAILED = 1;

Expand Down Expand Up @@ -124,8 +123,7 @@ contract SingleSignerValidationModule is IValidationModule, ReplaySafeWrapper, M
override
returns (bytes4)
{
bytes32 _replaySafeHash = replaySafeHash(account, digest);
if (_checkSig(signers[entityId][account], _replaySafeHash, signature)) {
if (_checkSig(signers[entityId][account], digest, signature)) {
return _1271_MAGIC_VALUE;
}
return _1271_INVALID;
Expand Down
6 changes: 2 additions & 4 deletions src/modules/validation/WebAuthnValidationModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ pragma solidity ^0.8.28;

import {IModule} from "@erc6900/reference-implementation/interfaces/IModule.sol";
import {IValidationModule} from "@erc6900/reference-implementation/interfaces/IValidationModule.sol";
import {ReplaySafeWrapper} from "@erc6900/reference-implementation/modules/ReplaySafeWrapper.sol";
import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";
import {WebAuthn} from "webauthn-sol/src/WebAuthn.sol";
Expand All @@ -38,7 +37,7 @@ import {ModuleBase} from "../ModuleBase.sol";
/// - This validation supports ERC-1271. The signature is valid if it is signed by the owner's private key.
/// - This validation supports composition that other validation can relay on entities in this validation to
/// validate partially or fully.
contract WebAuthnValidationModule is IValidationModule, ReplaySafeWrapper, ModuleBase {
contract WebAuthnValidationModule is IValidationModule, ModuleBase {
using WebAuthn for WebAuthn.WebAuthnAuth;

struct PubKey {
Expand Down Expand Up @@ -120,8 +119,7 @@ contract WebAuthnValidationModule is IValidationModule, ReplaySafeWrapper, Modul
override
returns (bytes4)
{
bytes32 _replaySafeHash = replaySafeHash(account, digest);
if (_validateSignature(entityId, account, _replaySafeHash, signature)) {
if (_validateSignature(entityId, account, digest, signature)) {
return _1271_MAGIC_VALUE;
}
return _1271_INVALID;
Expand Down
Loading