-
Notifications
You must be signed in to change notification settings - Fork 31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(v2): Add signature enum to user operation signature validation #41
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,9 +4,9 @@ pragma solidity ^0.8.23; | |
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; | ||
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; | ||
import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; | ||
import {SIG_VALIDATION_FAILED} from "account-abstraction/core/Helpers.sol"; | ||
import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol"; | ||
import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOperation.sol"; | ||
import {SIG_VALIDATION_FAILED, SIG_VALIDATION_SUCCESS} from "account-abstraction/core/Helpers.sol"; | ||
import {CastLib} from "modular-account/helpers/CastLib.sol"; | ||
import {SetValue} from "modular-account/libraries/Constants.sol"; | ||
import {LinkedListSet, LinkedListSetLib} from "modular-account/libraries/LinkedListSetLib.sol"; | ||
|
@@ -30,6 +30,24 @@ contract MultiOwnerLightAccount is BaseLightAccount, CustomSlotInitializable { | |
bytes32 internal constant _INITIALIZABLE_STORAGE_POSITION = | ||
0xaa296a366a62f6551d3ddfceae892d1791068a359a0d3461aab99dfc6c5fd700; | ||
|
||
// Signature types used for user operation validation and ERC-1271 signature validation. | ||
// The enum value encodes the following bit layout: | ||
// | is contract signature? | 0b_______1 | ||
// | has address provided? | 0b______1_ | ||
// | is transparent EIP-712? | 0b_____1__ | ||
// | empty | 0b00000___ | ||
enum SignatureTypes { | ||
EOA, // 0 | ||
CONTRACT, // 1 | ||
empty_1, // skip 2 to align bitmap | ||
CONTRACT_WITH_ADDR, // 3 | ||
TRANSPARENT_EOA, // 4 | ||
TRANSPARENT_CONTRACT, // 5 | ||
empty_2, // skip 6 to align bitmap | ||
TRANSPARENT_CONTRACT_WITH_ADDR // 7 | ||
|
||
} | ||
|
||
struct LightAccountStorage { | ||
LinkedListSet owners; | ||
} | ||
|
@@ -54,6 +72,9 @@ contract MultiOwnerLightAccount is BaseLightAccount, CustomSlotInitializable { | |
/// @dev The owner to be removed does not exist. | ||
error OwnerDoesNotExist(address owner); | ||
|
||
/// @dev The signature type provided is not valid. | ||
error InvalidSignatureType(); | ||
|
||
constructor(IEntryPoint entryPoint_) CustomSlotInitializable(_INITIALIZABLE_STORAGE_POSITION) { | ||
_ENTRY_POINT = entryPoint_; | ||
_disableInitializers(); | ||
|
@@ -138,19 +159,64 @@ contract MultiOwnerLightAccount is BaseLightAccount, CustomSlotInitializable { | |
override | ||
returns (uint256 validationData) | ||
{ | ||
bytes32 signedHash = userOpHash.toEthSignedMessageHash(); | ||
bytes memory signature = userOp.signature; | ||
(address recovered, ECDSA.RecoverError error,) = signedHash.tryRecover(signature); | ||
if (error == ECDSA.RecoverError.NoError && _getStorage().owners.contains(recovered.toSetValue())) { | ||
return 0; | ||
} | ||
if (_isValidERC1271SignatureNow(userOpHash, signature)) { | ||
return 0; | ||
uint8 signatureType = uint8(userOp.signature[0]); | ||
if (signatureType == uint8(SignatureTypes.EOA)) { | ||
// EOA signature | ||
bytes32 signedHash = userOpHash.toEthSignedMessageHash(); | ||
bytes memory signature = userOp.signature[1:]; | ||
return _successToValidationData(_isValidEOAOwnerSignature(signedHash, signature)); | ||
} else if (signatureType == uint8(SignatureTypes.CONTRACT)) { | ||
// Contract signature without address | ||
bytes memory signature = userOp.signature[1:]; | ||
return _successToValidationData(_isValidContractOwnerSignatureNowLoop(userOpHash, signature)); | ||
} else if (signatureType == uint8(SignatureTypes.CONTRACT_WITH_ADDR)) { | ||
// Contract signature with address | ||
address contractOwner = address(bytes20(userOp.signature[1:21])); | ||
bytes memory signature = userOp.signature[21:]; | ||
return | ||
_successToValidationData(_isValidContractOwnerSignatureNowSingle(contractOwner, userOpHash, signature)); | ||
} | ||
return SIG_VALIDATION_FAILED; | ||
|
||
revert InvalidSignatureType(); | ||
} | ||
|
||
/// @notice Check if the signature is a valid by an EOA owner for the given digest. | ||
/// @dev Only supports 65-byte signatures, and uses the digest directly. | ||
/// @param digest The digest to be checked. | ||
/// @param signature The signature to be checked. | ||
/// @return True if the signature is valid and by an owner, false otherwise. | ||
function _isValidEOAOwnerSignature(bytes32 digest, bytes memory signature) internal view returns (bool) { | ||
(address recovered, ECDSA.RecoverError error,) = digest.tryRecover(signature); | ||
return error == ECDSA.RecoverError.NoError && _getStorage().owners.contains(recovered.toSetValue()); | ||
} | ||
|
||
/// @notice Check if the given verifier is a contract owner, and if the signature is a valid ERC-1271 signature by | ||
/// a contract owner for the given digest. | ||
/// @param contractOwner The address of the contract owner. | ||
/// @param digest The digest to be checked. | ||
/// @param signature The signature to be checked. | ||
/// @return True if the signature is valid and by an owner, false otherwise. | ||
function _isValidContractOwnerSignatureNowSingle(address contractOwner, bytes32 digest, bytes memory signature) | ||
internal | ||
view | ||
returns (bool) | ||
{ | ||
return SignatureChecker.isValidERC1271SignatureNow(contractOwner, digest, signature) | ||
&& _getStorage().owners.contains(contractOwner.toSetValue()); | ||
} | ||
|
||
function _isValidERC1271SignatureNow(bytes32 digest, bytes memory signature) internal view returns (bool) { | ||
/// @notice Check if the signature is a valid ERC-1271 signature by a contract owner for the given digest by | ||
/// checking all owners in a loop. | ||
/// @dev Susceptible to denial-of-service by a malicious owner contract. To avoid this, use a signature type that | ||
/// includes the owner address. | ||
/// @param digest The digest to be checked. | ||
/// @param signature The signature to be checked. | ||
/// @return True if the signature is valid and by an owner, false otherwise. | ||
function _isValidContractOwnerSignatureNowLoop(bytes32 digest, bytes memory signature) | ||
internal | ||
view | ||
returns (bool) | ||
{ | ||
LightAccountStorage storage _storage = _getStorage(); | ||
address[] memory owners_ = _storage.owners.getAll().toAddressArray(); | ||
uint256 length = owners_.length; | ||
|
@@ -162,6 +228,14 @@ contract MultiOwnerLightAccount is BaseLightAccount, CustomSlotInitializable { | |
return false; | ||
} | ||
|
||
/// @dev Convert a boolean success value to a validation data value. | ||
/// @param success The success value to be converted. | ||
/// @return validationData The validation data value. 0 if success is true, 1 (SIG_VALIDATION_FAILED) if | ||
/// success is false. | ||
function _successToValidationData(bool success) internal pure returns (uint256 validationData) { | ||
return success ? SIG_VALIDATION_SUCCESS : SIG_VALIDATION_FAILED; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I previously had this as just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was wondering the same thing. I think this reads better! |
||
} | ||
|
||
/// @dev The signature is valid if it is signed by the owner's private key (if the owner is an EOA) or if it is a | ||
/// valid ERC-1271 signature from the owner (if the owner is a contract). | ||
function _isValidSignature(bytes32 derivedHash, bytes calldata trimmedSignature) | ||
|
@@ -173,7 +247,7 @@ contract MultiOwnerLightAccount is BaseLightAccount, CustomSlotInitializable { | |
{ | ||
(address recovered, ECDSA.RecoverError error,) = derivedHash.tryRecover(trimmedSignature); | ||
return (error == ECDSA.RecoverError.NoError && _getStorage().owners.contains(recovered.toSetValue())) | ||
|| _isValidERC1271SignatureNow(derivedHash, trimmedSignature); | ||
|| _isValidContractOwnerSignatureNowLoop(derivedHash, trimmedSignature); | ||
} | ||
|
||
function _domainNameAndVersion() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we going to be using the bits for checks later?
If not, maybe it's better to just go with regularly incrementing enums.
If we are, maintaining enums might get messy as we add on new signature types. So maybe just maintain these flags in a library and introduce helper functions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was planning on using the bits during the 1271 signature checking, but I'll take a look and see if it makes sense to just drop this down to an enum.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok! Edited my earlier comment to suggest a library instead if we're using the bits.