Skip to content

Commit 0c98b9d

Browse files
committed
feat: use nested eip-712 approach for isValidSignature
1 parent 1d1c48d commit 0c98b9d

File tree

7 files changed

+594
-130
lines changed

7 files changed

+594
-130
lines changed

ext/solady/EIP712.sol

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.4;
3+
4+
/// @notice Contract for EIP-712 typed structured data hashing and signing.
5+
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/EIP712.sol)
6+
/// @author Modified from Solbase (https://github.com/Sol-DAO/solbase/blob/main/src/utils/EIP712.sol)
7+
/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/EIP712.sol)
8+
///
9+
/// @dev Note, this implementation:
10+
/// - Uses `address(this)` for the `verifyingContract` field.
11+
/// - Does NOT use the optional EIP-712 salt.
12+
/// - Does NOT use any EIP-712 extensions.
13+
/// This is for simplicity and to save gas.
14+
/// If you need to customize, please fork / modify accordingly.
15+
abstract contract EIP712 {
16+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
17+
/* CONSTANTS AND IMMUTABLES */
18+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
19+
20+
/// @dev `keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")`.
21+
bytes32 internal constant _DOMAIN_TYPEHASH =
22+
0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f;
23+
24+
uint256 private immutable _cachedThis;
25+
uint256 private immutable _cachedChainId;
26+
bytes32 private immutable _cachedNameHash;
27+
bytes32 private immutable _cachedVersionHash;
28+
bytes32 private immutable _cachedDomainSeparator;
29+
30+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
31+
/* CONSTRUCTOR */
32+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
33+
34+
/// @dev Cache the hashes for cheaper runtime gas costs.
35+
/// In the case of upgradeable contracts (i.e. proxies),
36+
/// or if the chain id changes due to a hard fork,
37+
/// the domain separator will be seamlessly calculated on-the-fly.
38+
constructor() {
39+
_cachedThis = uint256(uint160(address(this)));
40+
_cachedChainId = block.chainid;
41+
42+
string memory name;
43+
string memory version;
44+
if (!_domainNameAndVersionMayChange()) (name, version) = _domainNameAndVersion();
45+
bytes32 nameHash = _domainNameAndVersionMayChange() ? bytes32(0) : keccak256(bytes(name));
46+
bytes32 versionHash =
47+
_domainNameAndVersionMayChange() ? bytes32(0) : keccak256(bytes(version));
48+
_cachedNameHash = nameHash;
49+
_cachedVersionHash = versionHash;
50+
51+
bytes32 separator;
52+
if (!_domainNameAndVersionMayChange()) {
53+
/// @solidity memory-safe-assembly
54+
assembly {
55+
let m := mload(0x40) // Load the free memory pointer.
56+
mstore(m, _DOMAIN_TYPEHASH)
57+
mstore(add(m, 0x20), nameHash)
58+
mstore(add(m, 0x40), versionHash)
59+
mstore(add(m, 0x60), chainid())
60+
mstore(add(m, 0x80), address())
61+
separator := keccak256(m, 0xa0)
62+
}
63+
}
64+
_cachedDomainSeparator = separator;
65+
}
66+
67+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
68+
/* FUNCTIONS TO OVERRIDE */
69+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
70+
71+
/// @dev Please override this function to return the domain name and version.
72+
/// ```
73+
/// function _domainNameAndVersion()
74+
/// internal
75+
/// pure
76+
/// virtual
77+
/// returns (string memory name, string memory version)
78+
/// {
79+
/// name = "Solady";
80+
/// version = "1";
81+
/// }
82+
/// ```
83+
///
84+
/// Note: If the returned result may change after the contract has been deployed,
85+
/// you must override `_domainNameAndVersionMayChange()` to return true.
86+
function _domainNameAndVersion()
87+
internal
88+
view
89+
virtual
90+
returns (string memory name, string memory version);
91+
92+
/// @dev Returns if `_domainNameAndVersion()` may change
93+
/// after the contract has been deployed (i.e. after the constructor).
94+
/// Default: false.
95+
function _domainNameAndVersionMayChange() internal pure virtual returns (bool result) {}
96+
97+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
98+
/* HASHING OPERATIONS */
99+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
100+
101+
/// @dev Returns the EIP-712 domain separator.
102+
function _domainSeparator() internal view virtual returns (bytes32 separator) {
103+
if (_domainNameAndVersionMayChange()) {
104+
separator = _buildDomainSeparator();
105+
} else {
106+
separator = _cachedDomainSeparator;
107+
if (_cachedDomainSeparatorInvalidated()) separator = _buildDomainSeparator();
108+
}
109+
}
110+
111+
/// @dev Returns the hash of the fully encoded EIP-712 message for this domain,
112+
/// given `structHash`, as defined in
113+
/// https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct.
114+
///
115+
/// The hash can be used together with {ECDSA-recover} to obtain the signer of a message:
116+
/// ```
117+
/// bytes32 digest = _hashTypedData(keccak256(abi.encode(
118+
/// keccak256("Mail(address to,string contents)"),
119+
/// mailTo,
120+
/// keccak256(bytes(mailContents))
121+
/// )));
122+
/// address signer = ECDSA.recover(digest, signature);
123+
/// ```
124+
function _hashTypedData(bytes32 structHash) internal view virtual returns (bytes32 digest) {
125+
// We will use `digest` to store the domain separator to save a bit of gas.
126+
if (_domainNameAndVersionMayChange()) {
127+
digest = _buildDomainSeparator();
128+
} else {
129+
digest = _cachedDomainSeparator;
130+
if (_cachedDomainSeparatorInvalidated()) digest = _buildDomainSeparator();
131+
}
132+
/// @solidity memory-safe-assembly
133+
assembly {
134+
// Compute the digest.
135+
mstore(0x00, 0x1901000000000000) // Store "\x19\x01".
136+
mstore(0x1a, digest) // Store the domain separator.
137+
mstore(0x3a, structHash) // Store the struct hash.
138+
digest := keccak256(0x18, 0x42)
139+
// Restore the part of the free memory slot that was overwritten.
140+
mstore(0x3a, 0)
141+
}
142+
}
143+
144+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
145+
/* EIP-5267 OPERATIONS */
146+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
147+
148+
/// @dev See: https://eips.ethereum.org/EIPS/eip-5267
149+
function eip712Domain()
150+
public
151+
view
152+
virtual
153+
returns (
154+
bytes1 fields,
155+
string memory name,
156+
string memory version,
157+
uint256 chainId,
158+
address verifyingContract,
159+
bytes32 salt,
160+
uint256[] memory extensions
161+
)
162+
{
163+
fields = hex"0f"; // `0b01111`.
164+
(name, version) = _domainNameAndVersion();
165+
chainId = block.chainid;
166+
verifyingContract = address(this);
167+
salt = salt; // `bytes32(0)`.
168+
extensions = extensions; // `new uint256[](0)`.
169+
}
170+
171+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
172+
/* PRIVATE HELPERS */
173+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
174+
175+
/// @dev Returns the EIP-712 domain separator.
176+
function _buildDomainSeparator() private view returns (bytes32 separator) {
177+
// We will use `separator` to store the name hash to save a bit of gas.
178+
bytes32 versionHash;
179+
if (_domainNameAndVersionMayChange()) {
180+
(string memory name, string memory version) = _domainNameAndVersion();
181+
separator = keccak256(bytes(name));
182+
versionHash = keccak256(bytes(version));
183+
} else {
184+
separator = _cachedNameHash;
185+
versionHash = _cachedVersionHash;
186+
}
187+
/// @solidity memory-safe-assembly
188+
assembly {
189+
let m := mload(0x40) // Load the free memory pointer.
190+
mstore(m, _DOMAIN_TYPEHASH)
191+
mstore(add(m, 0x20), separator) // Name hash.
192+
mstore(add(m, 0x40), versionHash)
193+
mstore(add(m, 0x60), chainid())
194+
mstore(add(m, 0x80), address())
195+
separator := keccak256(m, 0xa0)
196+
}
197+
}
198+
199+
/// @dev Returns if the cached domain separator has been invalidated.
200+
function _cachedDomainSeparatorInvalidated() private view returns (bool result) {
201+
uint256 cachedChainId = _cachedChainId;
202+
uint256 cachedThis = _cachedThis;
203+
/// @solidity memory-safe-assembly
204+
assembly {
205+
result := iszero(and(eq(chainid(), cachedChainId), eq(address(), cachedThis)))
206+
}
207+
}
208+
}

src/LightAccount.sol

Lines changed: 26 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ pragma solidity ^0.8.23;
55
/* solhint-disable no-inline-assembly */
66
/* solhint-disable reason-string */
77

8-
import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
98
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
109
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
1110
import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
@@ -47,11 +46,6 @@ contract LightAccount is BaseLightAccount, CustomSlotInitializable {
4746
// keccak256(abi.encode(uint256(keccak256("light_account_v1.initializable")) - 1)) & ~bytes32(uint256(0xff));
4847
bytes32 internal constant _INITIALIZABLE_STORAGE_POSITION =
4948
0x33e4b41198cc5b8053630ed667ea7c0c4c873f7fc8d9a478b5d7259cec0a4a00;
50-
bytes32 internal constant _DOMAIN_SEPARATOR_TYPEHASH =
51-
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
52-
bytes32 internal constant _LA_MSG_TYPEHASH = keccak256("LightAccountMessage(bytes message)");
53-
bytes32 internal constant _NAME_HASH = keccak256("LightAccount");
54-
bytes32 internal constant _VERSION_HASH = keccak256("1");
5549

5650
struct LightAccountStorage {
5751
address owner;
@@ -102,48 +96,6 @@ contract LightAccount is BaseLightAccount, CustomSlotInitializable {
10296
return _getStorage().owner;
10397
}
10498

105-
/// @notice Returns the domain separator for this contract, as defined in the EIP-712 standard.
106-
/// @return bytes32 The domain separator hash.
107-
function domainSeparator() public view returns (bytes32) {
108-
return keccak256(
109-
abi.encode(
110-
_DOMAIN_SEPARATOR_TYPEHASH,
111-
_NAME_HASH, // name
112-
_VERSION_HASH, // version
113-
block.chainid, // chainId
114-
address(this) // verifying contract
115-
)
116-
);
117-
}
118-
119-
/// @notice Returns the pre-image of the message hash.
120-
/// @param message Message that should be encoded.
121-
/// @return Encoded message.
122-
function encodeMessageData(bytes memory message) public view returns (bytes memory) {
123-
bytes32 messageHash = keccak256(abi.encode(_LA_MSG_TYPEHASH, keccak256(message)));
124-
return abi.encodePacked("\x19\x01", domainSeparator(), messageHash);
125-
}
126-
127-
/// @notice Returns hash of a message that can be signed by owners.
128-
/// @param message Message that should be hashed.
129-
/// @return Message hash.
130-
function getMessageHash(bytes memory message) public view returns (bytes32) {
131-
return keccak256(encodeMessageData(message));
132-
}
133-
134-
/// @inheritdoc IERC1271
135-
/// @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
136-
/// valid ERC-1271 signature from the owner (if the owner is a contract). Note that unlike the signature validation
137-
/// used in `validateUserOp`, this does **not** wrap the digest in an "Ethereum Signed Message" envelope before
138-
/// checking the signature in the EOA-owner case.
139-
function isValidSignature(bytes32 hash, bytes memory signature) public view override returns (bytes4) {
140-
bytes32 messageHash = getMessageHash(abi.encode(hash));
141-
if (SignatureChecker.isValidSignatureNow(owner(), messageHash, signature)) {
142-
return _1271_MAGIC_VALUE;
143-
}
144-
return 0xffffffff;
145-
}
146-
14799
function _initialize(address owner_) internal virtual {
148100
if (owner_ == address(0)) {
149101
revert InvalidOwner(address(0));
@@ -172,19 +124,42 @@ contract LightAccount is BaseLightAccount, CustomSlotInitializable {
172124
override
173125
returns (uint256 validationData)
174126
{
175-
address _owner = owner();
127+
address owner_ = owner();
176128
bytes32 signedHash = userOpHash.toEthSignedMessageHash();
177129
bytes memory signature = userOp.signature;
178130
(address recovered, ECDSA.RecoverError error,) = signedHash.tryRecover(signature);
179131
if (
180-
(error == ECDSA.RecoverError.NoError && recovered == _owner)
181-
|| SignatureChecker.isValidERC1271SignatureNow(_owner, userOpHash, signature)
132+
(error == ECDSA.RecoverError.NoError && recovered == owner_)
133+
|| SignatureChecker.isValidERC1271SignatureNow(owner_, userOpHash, signature)
182134
) {
183135
return 0;
184136
}
185137
return SIG_VALIDATION_FAILED;
186138
}
187139

140+
/// @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
141+
/// valid ERC-1271 signature from the owner (if the owner is a contract).
142+
function _isValidSignature(bytes32 derivedHash, bytes calldata trimmedSignature)
143+
internal
144+
view
145+
virtual
146+
override
147+
returns (bool)
148+
{
149+
return SignatureChecker.isValidSignatureNow(owner(), derivedHash, trimmedSignature);
150+
}
151+
152+
function _domainNameAndVersion()
153+
internal
154+
view
155+
virtual
156+
override
157+
returns (string memory name, string memory version)
158+
{
159+
name = "LightAccount";
160+
version = "2";
161+
}
162+
188163
function _isFromOwner() internal view virtual override returns (bool) {
189164
return msg.sender == owner();
190165
}

0 commit comments

Comments
 (0)