Skip to content

Commit

Permalink
fix tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Filipp Makarov authored and Filipp Makarov committed Oct 29, 2024
1 parent fa90983 commit d073e59
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 107 deletions.
208 changes: 125 additions & 83 deletions contracts/base/ERC7739Validator.sol
Original file line number Diff line number Diff line change
@@ -1,37 +1,56 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import { IERC7739 } from "../interfaces/IERC7739.sol";
import { EIP712 } from "solady/utils/EIP712.sol";

/// @title ERC-7739: Nested Typed Data Sign Support for ERC-7579 Validators
interface IERC5267 {
function eip712Domain() external view returns (
bytes1 fields,
string memory name,
string memory version,
uint256 chainId,
address verifyingContract,
bytes32 salt,
uint256[] memory extensions
);
}

abstract contract ERC7739Validator is IERC7739 {
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) ||
_erc1271IsValidSignatureViaNestedEIP712(hash, signature) ||
_erc1271IsValidSignatureViaRPC(hash, signature);
function _erc1271IsValidSignatureWithSender(address sender, bytes32 hash, bytes calldata signature)
internal
view
virtual
returns (bytes4)
{
// detection request
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 All @@ -49,18 +68,27 @@ abstract contract ERC7739Validator is IERC7739 {
/// module's specific internal function to validate the signature
/// against credentials.
/// Override for your module's custom logic.
function _erc1271IsValidSignatureNowCalldata(bytes32 hash, bytes calldata signature) internal view virtual returns (bool);
function _erc1271IsValidSignatureNowCalldata(bytes32 hash, bytes calldata signature)
internal
view
virtual
returns (bool);

/// @dev Unwraps and returns the signature.
function _erc1271UnwrapSignature(bytes calldata signature) internal view virtual returns (bytes calldata result) {
function _erc1271UnwrapSignature(bytes calldata signature)
internal
view
virtual
returns (bytes calldata result)
{
result = signature;
/// @solidity memory-safe-assembly
assembly {
// Unwraps the ERC6492 wrapper if it exists.
// 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 All @@ -71,11 +99,12 @@ abstract contract ERC7739Validator is IERC7739 {

/// @dev Performs the signature validation without nested EIP-712 if the caller is
/// a safe caller. A safe caller must include the address of this account in the hash.
function _erc1271IsValidSignatureViaSafeCaller(
address sender,
bytes32 hash,
bytes calldata signature
) internal view virtual returns (bool result) {
function _erc1271IsValidSignatureViaSafeCaller(address sender, bytes32 hash, bytes calldata signature)
internal
view
virtual
returns (bool result)
{
if (_erc1271CallerIsSafe(sender)) result = _erc1271IsValidSignatureNowCalldata(hash, signature);
}

Expand Down Expand Up @@ -108,17 +137,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 @@ -150,15 +184,24 @@ abstract contract ERC7739Validator is IERC7739 {
/// All these are just for widespread out-of-the-box compatibility with other wallet clients.
/// We want to create bazaars, not walled castles.
/// And we'll use push the Turing Completeness of the EVM to the limits to do so.
function _erc1271IsValidSignatureViaNestedEIP712(bytes32 hash, bytes calldata signature) internal view virtual returns (bool result) {
// Review
// bytes32 tfa = _typedDataSignFieldsForAccount(msg.sender);
bytes calldata contents = signature;
function _erc1271IsValidSignatureViaNestedEIP712(bytes32 hash, bytes calldata signature)
internal
view
virtual
returns (bool result)
{
//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,) =
EIP712(msg.sender).eip712Domain();
(
,
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.
Expand All @@ -171,6 +214,7 @@ abstract contract ERC7739Validator is IERC7739 {
mstore(0x40, add(t, 0xe0)) // Allocate the memory.
}
}

/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40) // Cache the free memory pointer.
Expand All @@ -181,28 +225,38 @@ abstract contract ERC7739Validator is IERC7739 {
let o := add(signature.offset, sub(signature.length, l)) // Offset of appended data.
mstore(0x00, 0x1901) // Store the "\x19\x01" prefix.
calldatacopy(0x20, o, 0x40) // Copy the `APP_DOMAIN_SEPARATOR` and `contents` struct hash.
// Only use the `TypedDataSign` workflow.
// `TypedDataSign({ContentsName} contents,bytes1 fields,...){ContentsType}`.
// Use the `PersonalSign` workflow if the reconstructed hash doesn't match,
// or if the appended data is invalid, i.e.
// `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)
mstore(0x20, hash) // Store the `prefixed`.
hash := keccak256(t, 0x40) // Compute the `PersonalSign` struct hash.
break
}
// Else, use the `TypedDataSign` workflow.
// `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 `contentsName`, optimistically.
contents.offset := add(o, 0x40) // Set the offset of `contents`.
contents.length := c // Set the length of `contents`.
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 { } {
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(]`.
// Advance `p` until we encounter '('.
for { } iszero(eq(byte(0, mload(p)), 40)) { p := add(p, 1) } {
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,string name,string") // Store the rest of the encoding.
Expand All @@ -223,14 +277,18 @@ abstract contract ERC7739Validator is IERC7739 {
}
mstore(0x40, m) // Restore the free memory pointer.
}
// Review
// if (tfa == bytes32(0)) hash = _hashTypedDataForAccount(msg.sender, hash); // `PersonalSign` workflow.
if (t == uint256(0)) hash = _hashTypedDataForAccount(msg.sender, hash); // `PersonalSign` workflow.
result = _erc1271IsValidSignatureNowCalldata(hash, signature);
}

/// @dev Performs the signature validation without nested EIP-712 to allow for easy sign ins.
/// This function must always return false or revert if called on-chain.
function _erc1271IsValidSignatureViaRPC(bytes32 hash, bytes calldata signature) internal view virtual returns (bool result) {
function _erc1271IsValidSignatureViaRPC(bytes32 hash, bytes calldata signature)
internal
view
virtual
returns (bool result)
{
// Non-zero gasprice is a heuristic to check if a call is on-chain,
// but we can't fully depend on it because it can be manipulated.
// See: https://x.com/NoahCitron/status/1580359718341484544
Expand All @@ -249,9 +307,7 @@ abstract contract ERC7739Validator is IERC7739 {
mstore(0x40, 0x40)
let gasToBurn := or(add(0xffff, gaslimit()), gaslimit())
// Burns gas computationally efficiently. Also, requires that `gas > gasToBurn`.
if or(eq(hash, b), lt(gas(), gasToBurn)) {
invalid()
}
if or(eq(hash, b), lt(gas(), gasToBurn)) { invalid() }
// Make a call to this with `b`, efficiently burning the gas provided.
// No valid transaction can consume more than the gaslimit.
// See: https://ethereum.github.io/yellowpaper/paper.pdf
Expand All @@ -264,46 +320,20 @@ 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
) = EIP712(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
/// @param structHash the typed data struct hash
function _hashTypedDataForAccount(address account, bytes32 structHash) private view returns (bytes32 digest) {
(
,
/*bytes1 fields*/ string memory name,
/*bytes1 fields*/,
string memory name,
string memory version,
uint256 chainId,
address verifyingContract /*bytes32 salt*/ /*uint256[] memory extensions*/,
,

) = EIP712(account).eip712Domain();
address verifyingContract,
/*bytes32 salt*/,
/*uint256[] memory extensions*/
) = IERC5267(account).eip712Domain();

/// @solidity memory-safe-assembly
assembly {
Expand All @@ -325,4 +355,16 @@ abstract contract ERC7739Validator is IERC7739 {
mstore(0x3a, 0)
}
}

/// @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);
}

}
9 changes: 1 addition & 8 deletions contracts/mocks/MockValidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,7 @@ contract MockValidator is ERC7739Validator {
) external view virtual returns (bytes4 sigValidationResult) {
// 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, _erc1271UnwrapSignature(signature));
}

// ISessionValidator interface for smart session
Expand Down
9 changes: 1 addition & 8 deletions contracts/modules/validators/K1Validator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,7 @@ contract K1Validator is IValidator, ERC7739Validator {
bytes32 hash,
bytes calldata signature
) external view virtual override returns (bytes4 sigValidationResult) {
// 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, _erc1271UnwrapSignature(signature));
}

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

0 comments on commit d073e59

Please sign in to comment.