Skip to content

Commit a3a9e8c

Browse files
authored
Add ERC7739 and ERC7739Utils (#5664)
1 parent 08566bf commit a3a9e8c

File tree

11 files changed

+821
-1
lines changed

11 files changed

+821
-1
lines changed

.changeset/lucky-donuts-scream.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`ERC7739`: An abstract contract to validate signatures following the rehashing scheme from `ERC7739Utils`.

.changeset/proud-tables-sip.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`ERC7739Utils`: Add a library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on the ERC-7739.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {ECDSA} from "../../../utils/cryptography/ECDSA.sol";
6+
import {EIP712} from "../../../utils/cryptography/EIP712.sol";
7+
import {ERC7739} from "../../../utils/cryptography/ERC7739.sol";
8+
import {AbstractSigner} from "../../../utils/cryptography/AbstractSigner.sol";
9+
10+
contract ERC7739ECDSAMock is AbstractSigner, ERC7739 {
11+
address private _signer;
12+
13+
constructor(address signerAddr) EIP712("ERC7739ECDSA", "1") {
14+
_signer = signerAddr;
15+
}
16+
17+
function signer() public view virtual returns (address) {
18+
return _signer;
19+
}
20+
21+
function _rawSignatureValidation(
22+
bytes32 hash,
23+
bytes calldata signature
24+
) internal view virtual override returns (bool) {
25+
(address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature);
26+
return signer() == recovered && err == ECDSA.RecoverError.NoError;
27+
}
28+
}

contracts/utils/README.adoc

+12-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ Miscellaneous contracts and libraries containing utility functions you can use t
4646
* {Comparators}: A library that contains comparator functions to use with the {Heap} library.
4747
* {CAIP2}, {CAIP10}: Libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers.
4848
* {Blockhash}: A library for accessing historical block hashes beyond the standard 256 block limit utilizing EIP-2935's historical blockhash functionality.
49-
49+
* {AbstractSigner}: Abstract contract for internal signature validation in smart contracts.
50+
* {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from `ERC7739Utils`.
51+
* {ERC7739Utils}: Utilities library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on ERC-7739.
52+
5053
[NOTE]
5154
====
5255
Because Solidity does not support generic types, {EnumerableMap} and {EnumerableSet} are specialized to a limited number of key-value types.
@@ -78,6 +81,14 @@ Because Solidity does not support generic types, {EnumerableMap} and {Enumerable
7881

7982
{{MerkleProof}}
8083

84+
{{ERC7739}}
85+
86+
{{ERC7739Utils}}
87+
88+
=== Abstract Signers
89+
90+
{{AbstractSigner}}
91+
8192
== Security
8293

8394
{{ReentrancyGuard}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
/**
6+
* @dev Abstract contract for signature validation.
7+
*
8+
* Developers must implement {_rawSignatureValidation} and use it as the lowest-level signature validation mechanism.
9+
*
10+
* @custom:stateless
11+
*/
12+
abstract contract AbstractSigner {
13+
/**
14+
* @dev Signature validation algorithm.
15+
*
16+
* WARNING: Implementing a signature validation algorithm is a security-sensitive operation as it involves
17+
* cryptographic verification. It is important to review and test thoroughly before deployment. Consider
18+
* using one of the signature verification libraries (xref:api:utils#ECDSA[ECDSA], xref:api:utils#P256[P256]
19+
* or xref:api:utils#RSA[RSA]).
20+
*/
21+
function _rawSignatureValidation(bytes32 hash, bytes calldata signature) internal view virtual returns (bool);
22+
}
+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {IERC1271} from "../../interfaces/IERC1271.sol";
6+
import {EIP712} from "../cryptography/EIP712.sol";
7+
import {MessageHashUtils} from "../cryptography/MessageHashUtils.sol";
8+
import {ShortStrings} from "../ShortStrings.sol";
9+
import {AbstractSigner} from "./AbstractSigner.sol";
10+
import {ERC7739Utils} from "./ERC7739Utils.sol";
11+
12+
/**
13+
* @dev Validates signatures wrapping the message hash in a nested EIP712 type. See {ERC7739Utils}.
14+
*
15+
* Linking the signature to the EIP-712 domain separator is a security measure to prevent signature replay across different
16+
* EIP-712 domains (e.g. a single offchain owner of multiple contracts).
17+
*
18+
* This contract requires implementing the {_rawSignatureValidation} function, which passes the wrapped message hash,
19+
* which may be either an typed data or a personal sign nested type.
20+
*
21+
* NOTE: xref:api:utils#EIP712[EIP-712] uses xref:api:utils#ShortStrings[ShortStrings] to optimize gas
22+
* costs for short strings (up to 31 characters). Consider that strings longer than that will use storage,
23+
* which may limit the ability of the signer to be used within the ERC-4337 validation phase (due to
24+
* https://eips.ethereum.org/EIPS/eip-7562#storage-rules[ERC-7562 storage access rules]).
25+
*/
26+
abstract contract ERC7739 is AbstractSigner, EIP712, IERC1271 {
27+
using ERC7739Utils for *;
28+
using MessageHashUtils for bytes32;
29+
30+
/**
31+
* @dev Attempts validating the signature in a nested EIP-712 type.
32+
*
33+
* A nested EIP-712 type might be presented in 2 different ways:
34+
*
35+
* - As a nested EIP-712 typed data
36+
* - As a _personal_ signature (an EIP-712 mimic of the `eth_personalSign` for a smart contract)
37+
*/
38+
function isValidSignature(bytes32 hash, bytes calldata signature) public view virtual returns (bytes4 result) {
39+
// For the hash `0x7739773977397739773977397739773977397739773977397739773977397739` and an empty signature,
40+
// we return the magic value `0x77390001` as it's assumed impossible to find a preimage for it that can be used
41+
// maliciously. Useful for simulation purposes and to validate whether the contract supports ERC-7739.
42+
return
43+
(_isValidNestedTypedDataSignature(hash, signature) || _isValidNestedPersonalSignSignature(hash, signature))
44+
? IERC1271.isValidSignature.selector
45+
: (hash == 0x7739773977397739773977397739773977397739773977397739773977397739 && signature.length == 0)
46+
? bytes4(0x77390001)
47+
: bytes4(0xffffffff);
48+
}
49+
50+
/**
51+
* @dev Nested personal signature verification.
52+
*/
53+
function _isValidNestedPersonalSignSignature(bytes32 hash, bytes calldata signature) private view returns (bool) {
54+
return _rawSignatureValidation(_domainSeparatorV4().toTypedDataHash(hash.personalSignStructHash()), signature);
55+
}
56+
57+
/**
58+
* @dev Nested EIP-712 typed data verification.
59+
*/
60+
function _isValidNestedTypedDataSignature(
61+
bytes32 hash,
62+
bytes calldata encodedSignature
63+
) private view returns (bool) {
64+
// decode signature
65+
(
66+
bytes calldata signature,
67+
bytes32 appSeparator,
68+
bytes32 contentsHash,
69+
string calldata contentsDescr
70+
) = encodedSignature.decodeTypedDataSig();
71+
72+
(
73+
,
74+
string memory name,
75+
string memory version,
76+
uint256 chainId,
77+
address verifyingContract,
78+
bytes32 salt,
79+
80+
) = eip712Domain();
81+
82+
// Check that contentHash and separator are correct
83+
// Rebuild nested hash
84+
return
85+
hash == appSeparator.toTypedDataHash(contentsHash) &&
86+
bytes(contentsDescr).length != 0 &&
87+
_rawSignatureValidation(
88+
appSeparator.toTypedDataHash(
89+
ERC7739Utils.typedDataSignStructHash(
90+
contentsDescr,
91+
contentsHash,
92+
abi.encode(keccak256(bytes(name)), keccak256(bytes(version)), chainId, verifyingContract, salt)
93+
)
94+
),
95+
signature
96+
);
97+
}
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {Calldata} from "../Calldata.sol";
6+
7+
/**
8+
* @dev Utilities to process https://ercs.ethereum.org/ERCS/erc-7739[ERC-7739] typed data signatures
9+
* that are specific to an EIP-712 domain.
10+
*
11+
* This library provides methods to wrap, unwrap and operate over typed data signatures with a defensive
12+
* rehashing mechanism that includes the application's xref:api:utils#EIP712-_domainSeparatorV4[EIP-712]
13+
* and preserves readability of the signed content using an EIP-712 nested approach.
14+
*
15+
* A smart contract domain can validate a signature for a typed data structure in two ways:
16+
*
17+
* - As an application validating a typed data signature. See {typedDataSignStructHash}.
18+
* - As a smart contract validating a raw message signature. See {personalSignStructHash}.
19+
*
20+
* NOTE: A provider for a smart contract wallet would need to return this signature as the
21+
* result of a call to `personal_sign` or `eth_signTypedData`, and this may be unsupported by
22+
* API clients that expect a return value of 129 bytes, or specifically the `r,s,v` parameters
23+
* of an xref:api:utils#ECDSA[ECDSA] signature, as is for example specified for
24+
* xref:api:utils#EIP712[EIP-712].
25+
*/
26+
library ERC7739Utils {
27+
/**
28+
* @dev An EIP-712 type to represent "personal" signatures
29+
* (i.e. mimic of `personal_sign` for smart contracts).
30+
*/
31+
bytes32 private constant PERSONAL_SIGN_TYPEHASH = keccak256("PersonalSign(bytes prefixed)");
32+
33+
/**
34+
* @dev Nest a signature for a given EIP-712 type into a nested signature for the domain of the app.
35+
*
36+
* Counterpart of {decodeTypedDataSig} to extract the original signature and the nested components.
37+
*/
38+
function encodeTypedDataSig(
39+
bytes memory signature,
40+
bytes32 appSeparator,
41+
bytes32 contentsHash,
42+
string memory contentsDescr
43+
) internal pure returns (bytes memory) {
44+
return
45+
abi.encodePacked(signature, appSeparator, contentsHash, contentsDescr, uint16(bytes(contentsDescr).length));
46+
}
47+
48+
/**
49+
* @dev Parses a nested signature into its components.
50+
*
51+
* Constructed as follows:
52+
*
53+
* `signature ‖ APP_DOMAIN_SEPARATOR ‖ contentsHash ‖ contentsDescr ‖ uint16(contentsDescr.length)`
54+
*
55+
* - `signature` is the signature for the (ERC-7739) nested struct hash. This signature indirectly signs over the
56+
* original "contents" hash (from the app) and the account's domain separator.
57+
* - `APP_DOMAIN_SEPARATOR` is the EIP-712 {EIP712-_domainSeparatorV4} of the application smart contract that is
58+
* requesting the signature verification (though ERC-1271).
59+
* - `contentsHash` is the hash of the underlying data structure or message.
60+
* - `contentsDescr` is a descriptor of the "contents" part of the the EIP-712 type of the nested signature.
61+
*
62+
* NOTE: This function returns empty if the input format is invalid instead of reverting.
63+
* data instead.
64+
*/
65+
function decodeTypedDataSig(
66+
bytes calldata encodedSignature
67+
)
68+
internal
69+
pure
70+
returns (bytes calldata signature, bytes32 appSeparator, bytes32 contentsHash, string calldata contentsDescr)
71+
{
72+
unchecked {
73+
uint256 sigLength = encodedSignature.length;
74+
75+
// 66 bytes = contentsDescrLength (2 bytes) + contentsHash (32 bytes) + APP_DOMAIN_SEPARATOR (32 bytes).
76+
if (sigLength < 66) return (Calldata.emptyBytes(), 0, 0, Calldata.emptyString());
77+
78+
uint256 contentsDescrEnd = sigLength - 2; // Last 2 bytes
79+
uint256 contentsDescrLength = uint16(bytes2(encodedSignature[contentsDescrEnd:]));
80+
81+
// Check for space for `contentsDescr` in addition to the 66 bytes documented above
82+
if (sigLength < 66 + contentsDescrLength) return (Calldata.emptyBytes(), 0, 0, Calldata.emptyString());
83+
84+
uint256 contentsHashEnd = contentsDescrEnd - contentsDescrLength;
85+
uint256 separatorEnd = contentsHashEnd - 32;
86+
uint256 signatureEnd = separatorEnd - 32;
87+
88+
signature = encodedSignature[:signatureEnd];
89+
appSeparator = bytes32(encodedSignature[signatureEnd:separatorEnd]);
90+
contentsHash = bytes32(encodedSignature[separatorEnd:contentsHashEnd]);
91+
contentsDescr = string(encodedSignature[contentsHashEnd:contentsDescrEnd]);
92+
}
93+
}
94+
95+
/**
96+
* @dev Nests an `ERC-191` digest into a `PersonalSign` EIP-712 struct, and returns the corresponding struct hash.
97+
* This struct hash must be combined with a domain separator, using {MessageHashUtils-toTypedDataHash} before
98+
* being verified/recovered.
99+
*
100+
* This is used to simulates the `personal_sign` RPC method in the context of smart contracts.
101+
*/
102+
function personalSignStructHash(bytes32 contents) internal pure returns (bytes32) {
103+
return keccak256(abi.encode(PERSONAL_SIGN_TYPEHASH, contents));
104+
}
105+
106+
/**
107+
* @dev Nests an `EIP-712` hash (`contents`) into a `TypedDataSign` EIP-712 struct, and returns the corresponding
108+
* struct hash. This struct hash must be combined with a domain separator, using {MessageHashUtils-toTypedDataHash}
109+
* before being verified/recovered.
110+
*/
111+
function typedDataSignStructHash(
112+
string calldata contentsName,
113+
string calldata contentsType,
114+
bytes32 contentsHash,
115+
bytes memory domainBytes
116+
) internal pure returns (bytes32 result) {
117+
return
118+
bytes(contentsName).length == 0
119+
? bytes32(0)
120+
: keccak256(
121+
abi.encodePacked(typedDataSignTypehash(contentsName, contentsType), contentsHash, domainBytes)
122+
);
123+
}
124+
125+
/**
126+
* @dev Variant of {typedDataSignStructHash-string-string-bytes32-bytes} that takes a content descriptor
127+
* and decodes the `contentsName` and `contentsType` out of it.
128+
*/
129+
function typedDataSignStructHash(
130+
string calldata contentsDescr,
131+
bytes32 contentsHash,
132+
bytes memory domainBytes
133+
) internal pure returns (bytes32 result) {
134+
(string calldata contentsName, string calldata contentsType) = decodeContentsDescr(contentsDescr);
135+
136+
return typedDataSignStructHash(contentsName, contentsType, contentsHash, domainBytes);
137+
}
138+
139+
/**
140+
* @dev Compute the EIP-712 typehash of the `TypedDataSign` structure for a given type (and typename).
141+
*/
142+
function typedDataSignTypehash(
143+
string calldata contentsName,
144+
string calldata contentsType
145+
) internal pure returns (bytes32) {
146+
return
147+
keccak256(
148+
abi.encodePacked(
149+
"TypedDataSign(",
150+
contentsName,
151+
" contents,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)",
152+
contentsType
153+
)
154+
);
155+
}
156+
157+
/**
158+
* @dev Parse the type name out of the ERC-7739 contents type description. Supports both the implicit and explicit
159+
* modes.
160+
*
161+
* Following ERC-7739 specifications, a `contentsName` is considered invalid if it's empty or it contains
162+
* any of the following bytes , )\x00
163+
*
164+
* If the `contentsType` is invalid, this returns an empty string. Otherwise, the return string has non-zero
165+
* length.
166+
*/
167+
function decodeContentsDescr(
168+
string calldata contentsDescr
169+
) internal pure returns (string calldata contentsName, string calldata contentsType) {
170+
bytes calldata buffer = bytes(contentsDescr);
171+
if (buffer.length == 0) {
172+
// pass through (fail)
173+
} else if (buffer[buffer.length - 1] == bytes1(")")) {
174+
// Implicit mode: read contentsName from the beginning, and keep the complete descr
175+
for (uint256 i = 0; i < buffer.length; ++i) {
176+
bytes1 current = buffer[i];
177+
if (current == bytes1("(")) {
178+
// if name is empty - passthrough (fail)
179+
if (i == 0) break;
180+
// we found the end of the contentsName
181+
return (string(buffer[:i]), contentsDescr);
182+
} else if (_isForbiddenChar(current)) {
183+
// we found an invalid character (forbidden) - passthrough (fail)
184+
break;
185+
}
186+
}
187+
} else {
188+
// Explicit mode: read contentsName from the end, and remove it from the descr
189+
for (uint256 i = buffer.length; i > 0; --i) {
190+
bytes1 current = buffer[i - 1];
191+
if (current == bytes1(")")) {
192+
// we found the end of the contentsName
193+
return (string(buffer[i:]), string(buffer[:i]));
194+
} else if (_isForbiddenChar(current)) {
195+
// we found an invalid character (forbidden) - passthrough (fail)
196+
break;
197+
}
198+
}
199+
}
200+
return (Calldata.emptyString(), Calldata.emptyString());
201+
}
202+
203+
function _isForbiddenChar(bytes1 char) private pure returns (bool) {
204+
return char == 0x00 || char == bytes1(" ") || char == bytes1(",") || char == bytes1("(") || char == bytes1(")");
205+
}
206+
}

0 commit comments

Comments
 (0)