Skip to content

Commit

Permalink
Merge pull request #2 from erc7579/fix/update-erc7739
Browse files Browse the repository at this point in the history
Update ERC-7739
  • Loading branch information
filmakarov authored Oct 30, 2024
2 parents 633d1bb + b3f98b8 commit d0926bd
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 88 deletions.
169 changes: 101 additions & 68 deletions src/ERC7739Validator.sol
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

/// @title ERC-7739: Nested Typed Data Sign Support for ERC-7579 Validators
interface IERC7739 {
// @notice Returns magic value if this module supports ERC-7739
function supportsNestedTypedDataSign() external view returns (bytes32);
}

interface IERC5267 {
function eip712Domain() external view returns (
bytes1 fields,
Expand All @@ -19,38 +13,51 @@ interface IERC5267 {
);
}

abstract contract ERC7739Validator is IERC7739 {
/// @title ERC-7739: Nested Typed Data Sign Support for ERC-7579 Validators
abstract contract ERC7739Validator {
/// @dev `keccak256("PersonalSign(bytes prefixed)")`.
bytes32 internal constant _PERSONAL_SIGN_TYPEHASH = 0x983e65e5148e570cd828ead231ee759a8d7958721a768f93bc4483ba005c32de;
bytes32 internal constant _DOMAIN_TYPEHASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f;


/// @dev For automatic detection that the smart account supports the nested EIP-712 workflow.
/// By default, it returns `bytes32(bytes4(keccak256("supportsNestedTypedDataSign()")))`,
/// denoting support for the default behavior, as implemented in
/// `_erc1271IsValidSignatureViaNestedEIP712`, which is called in `isValidSignature`.
/// Future extensions should return a different non-zero `result` to denote different behavior.
/// This method intentionally returns bytes32 to allow freedom for future extensions.
function supportsNestedTypedDataSign() public view virtual returns (bytes32 result) {
result = bytes4(0xd620c85a);
}
bytes4 internal constant SUPPORTS_ERC7739 = 0x77390001;

/*//////////////////////////////////////////////////////////////////////////
INTERNAL
//////////////////////////////////////////////////////////////////////////*/


/// @dev Returns whether the `signature` is valid for the `hash.
/// Use this in your validator's `isValidSignatureWithSender` implementation.
function _erc1271IsValidSignatureWithSender(address sender, bytes32 hash, bytes calldata signature)
internal
view
virtual
returns (bool)
{
return _erc1271IsValidSignatureViaSafeCaller(sender, hash, signature)
returns (bytes4)
{
// detection request
// this check only takes 17 gas units
// in theory, it can be moved out of this function so it doesn't apply to every
// isValidSignatureWithSender() call, but it would require an additional standard
// interface for SA to check if the IValidator supports ERC-7739
// while isValidSignatureWithSender() is specified by ERC-7579, so
// it makes sense to use it in SA to check if the validator supports ERC-7739
unchecked {
if (signature.length == uint256(0)) {
// Forces the compiler to optimize for smaller bytecode size.
if (uint256(hash) == ~signature.length / 0xffff * 0x7739)
return SUPPORTS_ERC7739;
}
}

bool success = _erc1271IsValidSignatureViaSafeCaller(sender, hash, signature)
|| _erc1271IsValidSignatureViaNestedEIP712(hash, signature)
|| _erc1271IsValidSignatureViaRPC(hash, signature);

bytes4 sigValidationResult;
assembly {
// `success ? bytes4(keccak256("isValidSignature(bytes32,bytes)")) : 0xffffffff`.
// We use `0xffffffff` for invalid, in convention with the reference implementation.
sigValidationResult := shl(224, or(0x1626ba7e, sub(0, iszero(success))))
}
return sigValidationResult;
}

/// @dev Returns whether the `msg.sender` is considered safe, such
Expand Down Expand Up @@ -88,7 +95,7 @@ abstract contract ERC7739Validator is IERC7739 {
// See: https://eips.ethereum.org/EIPS/eip-6492
if eq(
calldataload(add(result.offset, sub(result.length, 0x20))),
mul(0x6492, div(not(mload(0x60)), 0xffff)) // `0x6492...6492`.
mul(0x6492, div(not(shr(address(), address())), 0xffff)) // `0x6492...6492`.
) {
let o := add(result.offset, calldataload(add(result.offset, 0x40)))
result.length := calldataload(o)
Expand Down Expand Up @@ -137,17 +144,22 @@ abstract contract ERC7739Validator is IERC7739 {
/// version: keccak256(bytes(eip712Domain().version)),
/// chainId: eip712Domain().chainId,
/// verifyingContract: eip712Domain().verifyingContract,
/// salt: eip712Domain().salt,
/// extensions: keccak256(abi.encodePacked(eip712Domain().extensions))
/// salt: eip712Domain().salt
/// }))
/// )
/// ```
/// where `‖` denotes the concatenation operator for bytes.
/// The order of the fields is important: `contents` comes before `name`.
///
/// The signature will be `r ‖ s ‖ v ‖
/// APP_DOMAIN_SEPARATOR ‖ contents ‖ contentsType ‖ uint16(contentsType.length)`,
/// where `contents` is the bytes32 struct hash of the original struct.
/// The signature will be `r ‖ s ‖ v ‖ APP_DOMAIN_SEPARATOR ‖
/// contents ‖ contentsDescription ‖ uint16(contentsDescription.length)`,
/// where:
/// - `contents` is the bytes32 struct hash of the original struct.
/// - `contentsDescription` can be either:
/// a) `contentsType` (implicit mode)
/// where `contentsType` starts with `contentsName`.
/// b) `contentsType ‖ contentsName` (explicit mode)
/// where `contentsType` may not necessarily start with `contentsName`.
///
/// The `APP_DOMAIN_SEPARATOR` and `contents` will be used to verify if `hash` is indeed correct.
/// __________________________________________________________________________________________
Expand Down Expand Up @@ -185,11 +197,35 @@ abstract contract ERC7739Validator is IERC7739 {
virtual
returns (bool result)
{
bytes32 t = _typedDataSignFieldsForAccount(msg.sender);
//bytes32 t = _typedDataSignFieldsForAccount(msg.sender);
uint256 t = uint256(uint160(address(this)));
// Forces the compiler to pop the variables after the scope, avoiding stack-too-deep.
if (t != uint256(0)) {
(
,
string memory name,
string memory version,
uint256 chainId,
address verifyingContract,
bytes32 salt,
) = IERC5267(msg.sender).eip712Domain();
/// @solidity memory-safe-assembly
assembly {
t := mload(0x40) // Grab the free memory pointer.
// Skip 2 words for the `typedDataSignTypehash` and `contents` struct hash.
mstore(add(t, 0x40), keccak256(add(name, 0x20), mload(name)))
mstore(add(t, 0x60), keccak256(add(version, 0x20), mload(version)))
mstore(add(t, 0x80), chainId)
mstore(add(t, 0xa0), shr(96, shl(96, verifyingContract)))
mstore(add(t, 0xc0), salt)
mstore(0x40, add(t, 0xe0)) // Allocate the memory.
}
}

/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40) // Cache the free memory pointer.
// `c` is `contentsType.length`, which is stored in the last 2 bytes of the signature.
// `c` is `contentsDescription.length`, which is stored in the last 2 bytes of the signature.
let c := shr(240, calldataload(add(signature.offset, sub(signature.length, 2))))
for {} 1 {} {
let l := add(0x42, c) // Total length of appended data (32 + 32 + c + 2).
Expand All @@ -198,7 +234,7 @@ abstract contract ERC7739Validator is IERC7739 {
calldatacopy(0x20, o, 0x40) // Copy the `APP_DOMAIN_SEPARATOR` and `contents` struct hash.
// Use the `PersonalSign` workflow if the reconstructed hash doesn't match,
// or if the appended data is invalid, i.e.
// `appendedData.length > signature.length || contentsType.length == 0`.
// `appendedData.length > signature.length || contentsDescription.length == 0`.
if or(xor(keccak256(0x1e, 0x42), hash), or(lt(signature.length, l), iszero(c))) {
t := 0 // Set `t` to 0, denoting that we need to `hash = _hashTypedData(hash)`.
mstore(t, _PERSONAL_SIGN_TYPEHASH)
Expand All @@ -207,36 +243,48 @@ abstract contract ERC7739Validator is IERC7739 {
break
}
// Else, use the `TypedDataSign` workflow.
// `TypedDataSign({ContentsName} contents,bytes1 fields,...){ContentsType}`.
// `TypedDataSign({ContentsName} contents,string name,...){ContentsType}`.
mstore(m, "TypedDataSign(") // Store the start of `TypedDataSign`'s type encoding.
let p := add(m, 0x0e) // Advance 14 bytes to skip "TypedDataSign(".
calldatacopy(p, add(o, 0x40), c) // Copy `contentsType` to extract `contentsName`.

calldatacopy(p, add(o, 0x40), c) // Copy `contentsName`, optimistically.
mstore(add(p, c), 40) // Store a '(' after the end.
if iszero(eq(byte(0, mload(sub(add(p, c), 1))), 41)) {
let e := 0 // Length of `contentsName` in explicit mode.
for { let q := sub(add(p, c), 1) } 1 {} {
e := add(e, 1) // Scan backwards until we encounter a ')'.
if iszero(gt(lt(e, c), eq(byte(0, mload(sub(q, e))), 41))) { break }
}
c := sub(c, e) // Truncate `contentsDescription` to `contentsType`.
calldatacopy(p, add(add(o, 0x40), c), e) // Copy `contentsName`.
mstore8(add(p, e), 40) // Store a '(' exactly right after the end.
}

// `d & 1 == 1` means that `contentsName` is invalid.
let d := shr(byte(0, mload(p)), 0x7fffffe000000000000010000000000) // Starts with `[a-z(]`.
// Store the end sentinel '(', and advance `p` until we encounter a '(' byte.
for { mstore(add(p, c), 40) } iszero(eq(byte(0, mload(p)), 40)) { p := add(p, 1) } {
// Advance `p` until we encounter '('.
for {} iszero(eq(byte(0, mload(p)), 40)) { p := add(p, 1) } {
d := or(shr(byte(0, mload(p)), 0x120100000001), d) // Has a byte in ", )\x00".
}
mstore(p, " contents,bytes1 fields,string n") // Store the rest of the encoding.
mstore(add(p, 0x20), "ame,string version,uint256 chain")
mstore(add(p, 0x40), "Id,address verifyingContract,byt")
mstore(add(p, 0x60), "es32 salt,uint256[] extensions)")
p := add(p, 0x7f)
mstore(p, " contents,string name,string") // Store the rest of the encoding.
mstore(add(p, 0x1c), " version,uint256 chainId,address")
mstore(add(p, 0x3c), " verifyingContract,bytes32 salt)")
p := add(p, 0x5c)
calldatacopy(p, add(o, 0x40), c) // Copy `contentsType`.
// Fill in the missing fields of the `TypedDataSign`.
calldatacopy(t, o, 0x40) // Copy the `contents` struct hash to `add(t, 0x20)`.
mstore(t, keccak256(m, sub(add(p, c), m))) // Store `typedDataSignTypehash`.
// The "\x19\x01" prefix is already at 0x00.
// `APP_DOMAIN_SEPARATOR` is already at 0x20.
mstore(0x40, keccak256(t, 0x120)) // `hashStruct(typedDataSign)`.
mstore(0x40, keccak256(t, 0xe0)) // `hashStruct(typedDataSign)`.
// Compute the final hash, corrupted if `contentsName` is invalid.
hash := keccak256(0x1e, add(0x42, and(1, d)))
signature.length := sub(signature.length, l) // Truncate the signature.
break
}
mstore(0x40, m) // Restore the free memory pointer.
}
if (t == bytes32(0)) hash = _hashTypedDataForAccount(msg.sender, hash); // `PersonalSign` workflow.
if (t == uint256(0)) hash = _hashTypedDataForAccount(msg.sender, hash); // `PersonalSign` workflow.
result = _erc1271IsValidSignatureNowCalldata(hash, signature);
}

Expand Down Expand Up @@ -279,32 +327,6 @@ abstract contract ERC7739Validator is IERC7739 {
}
}

/// @dev For use in `_erc1271IsValidSignatureViaNestedEIP712`,
function _typedDataSignFieldsForAccount(address account) private view returns (bytes32 m) {
(
bytes1 fields,
string memory name,
string memory version,
uint256 chainId,
address verifyingContract,
bytes32 salt,
uint256[] memory extensions
) = IERC5267(account).eip712Domain();
/// @solidity memory-safe-assembly
assembly {
m := mload(0x40) // Grab the free memory pointer.
mstore(0x40, add(m, 0x120)) // Allocate the memory.
// Skip 2 words for the `typedDataSignTypehash` and `contents` struct hash.
mstore(add(m, 0x40), shl(248, byte(0, fields)))
mstore(add(m, 0x60), keccak256(add(name, 0x20), mload(name)))
mstore(add(m, 0x80), keccak256(add(version, 0x20), mload(version)))
mstore(add(m, 0xa0), chainId)
mstore(add(m, 0xc0), shr(96, shl(96, verifyingContract)))
mstore(add(m, 0xe0), salt)
mstore(add(m, 0x100), keccak256(add(extensions, 0x20), shl(5, mload(extensions))))
}
}

/// @notice Hashes typed data according to eip-712
/// Uses account's domain separator
/// @param account the smart account, who's domain separator will be used
Expand Down Expand Up @@ -341,4 +363,15 @@ abstract contract ERC7739Validator is IERC7739 {
}
}

/// @dev Backwards compatibility stuff
/// For automatic detection that the smart account supports the nested EIP-712 workflow.
/// By default, it returns `bytes32(bytes4(keccak256("supportsNestedTypedDataSign()")))`,
/// denoting support for the default behavior, as implemented in
/// `_erc1271IsValidSignatureViaNestedEIP712`, which is called in `isValidSignature`.
/// Future extensions should return a different non-zero `result` to denote different behavior.
/// This method intentionally returns bytes32 to allow freedom for future extensions.
function supportsNestedTypedDataSign() public view virtual returns (bytes32 result) {
result = bytes4(0xd620c85a);
}

}
13 changes: 2 additions & 11 deletions src/SampleK1ValidatorWithERC7739.sol
Original file line number Diff line number Diff line change
Expand Up @@ -134,18 +134,9 @@ contract SampleK1ValidatorWithERC7739 is ERC7579ValidatorBase, ERC7739Validator
view
virtual
override
returns (bytes4 sigValidationResult)
returns (bytes4)
{
// can put additional checks based on sender here

// check if sig is valid
bool success = _erc1271IsValidSignatureWithSender(sender, hash, _erc1271UnwrapSignature(signature));
/// @solidity memory-safe-assembly
assembly {
// `success ? bytes4(keccak256("isValidSignature(bytes32,bytes)")) : 0xffffffff`.
// We use `0xffffffff` for invalid, in convention with the reference implementation.
sigValidationResult := shl(224, or(0x1626ba7e, sub(0, iszero(success))))
}
return _erc1271IsValidSignatureWithSender(sender, hash, signature);
}

/// @notice ISessionValidator interface for smart session
Expand Down
Loading

0 comments on commit d0926bd

Please sign in to comment.