draft-ERC7739Utils.sol 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. // SPDX-License-Identifier: MIT
  2. // OpenZeppelin Contracts (last updated v5.4.0) (utils/cryptography/draft-ERC7739Utils.sol)
  3. pragma solidity ^0.8.20;
  4. import {Calldata} from "../Calldata.sol";
  5. import {Hashes} from "./Hashes.sol";
  6. /**
  7. * @dev Utilities to process https://ercs.ethereum.org/ERCS/erc-7739[ERC-7739] typed data signatures
  8. * that are specific to an EIP-712 domain.
  9. *
  10. * This library provides methods to wrap, unwrap and operate over typed data signatures with a defensive
  11. * rehashing mechanism that includes the app's xref:api:utils/cryptography#EIP712-_domainSeparatorV4[EIP-712]
  12. * and preserves readability of the signed content using an EIP-712 nested approach.
  13. *
  14. * A smart contract domain can validate a signature for a typed data structure in two ways:
  15. *
  16. * - As an application validating a typed data signature. See {typedDataSignStructHash}.
  17. * - As a smart contract validating a raw message signature. See {personalSignStructHash}.
  18. *
  19. * NOTE: A provider for a smart contract wallet would need to return this signature as the
  20. * result of a call to `personal_sign` or `eth_signTypedData`, and this may be unsupported by
  21. * API clients that expect a return value of 129 bytes, or specifically the `r,s,v` parameters
  22. * of an xref:api:utils/cryptography#ECDSA[ECDSA] signature, as is for example specified for
  23. * xref:api:utils/cryptography#EIP712[EIP-712].
  24. */
  25. library ERC7739Utils {
  26. /**
  27. * @dev An EIP-712 type to represent "personal" signatures
  28. * (i.e. mimic of `personal_sign` for smart contracts).
  29. */
  30. bytes32 private constant PERSONAL_SIGN_TYPEHASH = keccak256("PersonalSign(bytes prefixed)");
  31. /**
  32. * @dev Nest a signature for a given EIP-712 type into a nested signature for the domain of the app.
  33. *
  34. * Counterpart of {decodeTypedDataSig} to extract the original signature and the nested components.
  35. */
  36. function encodeTypedDataSig(
  37. bytes memory signature,
  38. bytes32 appSeparator,
  39. bytes32 contentsHash,
  40. string memory contentsDescr
  41. ) internal pure returns (bytes memory) {
  42. return
  43. abi.encodePacked(signature, appSeparator, contentsHash, contentsDescr, uint16(bytes(contentsDescr).length));
  44. }
  45. /**
  46. * @dev Parses a nested signature into its components.
  47. *
  48. * Constructed as follows:
  49. *
  50. * `signature ‖ APP_DOMAIN_SEPARATOR ‖ contentsHash ‖ contentsDescr ‖ uint16(contentsDescr.length)`
  51. *
  52. * - `signature` is the signature for the (ERC-7739) nested struct hash. This signature indirectly signs over the
  53. * original "contents" hash (from the app) and the account's domain separator.
  54. * - `APP_DOMAIN_SEPARATOR` is the EIP-712 {EIP712-_domainSeparatorV4} of the application smart contract that is
  55. * requesting the signature verification (though ERC-1271).
  56. * - `contentsHash` is the hash of the underlying data structure or message.
  57. * - `contentsDescr` is a descriptor of the "contents" part of the the EIP-712 type of the nested signature.
  58. *
  59. * NOTE: This function returns empty if the input format is invalid instead of reverting.
  60. * data instead.
  61. */
  62. function decodeTypedDataSig(
  63. bytes calldata encodedSignature
  64. )
  65. internal
  66. pure
  67. returns (bytes calldata signature, bytes32 appSeparator, bytes32 contentsHash, string calldata contentsDescr)
  68. {
  69. unchecked {
  70. uint256 sigLength = encodedSignature.length;
  71. // 66 bytes = contentsDescrLength (2 bytes) + contentsHash (32 bytes) + APP_DOMAIN_SEPARATOR (32 bytes).
  72. if (sigLength < 66) return (Calldata.emptyBytes(), 0, 0, Calldata.emptyString());
  73. uint256 contentsDescrEnd = sigLength - 2; // Last 2 bytes
  74. uint256 contentsDescrLength = uint16(bytes2(encodedSignature[contentsDescrEnd:]));
  75. // Check for space for `contentsDescr` in addition to the 66 bytes documented above
  76. if (sigLength < 66 + contentsDescrLength) return (Calldata.emptyBytes(), 0, 0, Calldata.emptyString());
  77. uint256 contentsHashEnd = contentsDescrEnd - contentsDescrLength;
  78. uint256 separatorEnd = contentsHashEnd - 32;
  79. uint256 signatureEnd = separatorEnd - 32;
  80. signature = encodedSignature[:signatureEnd];
  81. appSeparator = bytes32(encodedSignature[signatureEnd:separatorEnd]);
  82. contentsHash = bytes32(encodedSignature[separatorEnd:contentsHashEnd]);
  83. contentsDescr = string(encodedSignature[contentsHashEnd:contentsDescrEnd]);
  84. }
  85. }
  86. /**
  87. * @dev Nests an `ERC-191` digest into a `PersonalSign` EIP-712 struct, and returns the corresponding struct hash.
  88. * This struct hash must be combined with a domain separator, using {MessageHashUtils-toTypedDataHash} before
  89. * being verified/recovered.
  90. *
  91. * This is used to simulates the `personal_sign` RPC method in the context of smart contracts.
  92. */
  93. function personalSignStructHash(bytes32 contents) internal pure returns (bytes32) {
  94. return Hashes.efficientKeccak256(PERSONAL_SIGN_TYPEHASH, contents);
  95. }
  96. /**
  97. * @dev Nests an `EIP-712` hash (`contents`) into a `TypedDataSign` EIP-712 struct, and returns the corresponding
  98. * struct hash. This struct hash must be combined with a domain separator, using {MessageHashUtils-toTypedDataHash}
  99. * before being verified/recovered.
  100. */
  101. function typedDataSignStructHash(
  102. string calldata contentsName,
  103. string calldata contentsType,
  104. bytes32 contentsHash,
  105. bytes memory domainBytes
  106. ) internal pure returns (bytes32 result) {
  107. return
  108. bytes(contentsName).length == 0
  109. ? bytes32(0)
  110. : keccak256(
  111. abi.encodePacked(typedDataSignTypehash(contentsName, contentsType), contentsHash, domainBytes)
  112. );
  113. }
  114. /**
  115. * @dev Variant of {typedDataSignStructHash-string-string-bytes32-bytes} that takes a content descriptor
  116. * and decodes the `contentsName` and `contentsType` out of it.
  117. */
  118. function typedDataSignStructHash(
  119. string calldata contentsDescr,
  120. bytes32 contentsHash,
  121. bytes memory domainBytes
  122. ) internal pure returns (bytes32 result) {
  123. (string calldata contentsName, string calldata contentsType) = decodeContentsDescr(contentsDescr);
  124. return typedDataSignStructHash(contentsName, contentsType, contentsHash, domainBytes);
  125. }
  126. /**
  127. * @dev Compute the EIP-712 typehash of the `TypedDataSign` structure for a given type (and typename).
  128. */
  129. function typedDataSignTypehash(
  130. string calldata contentsName,
  131. string calldata contentsType
  132. ) internal pure returns (bytes32) {
  133. return
  134. keccak256(
  135. abi.encodePacked(
  136. "TypedDataSign(",
  137. contentsName,
  138. " contents,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)",
  139. contentsType
  140. )
  141. );
  142. }
  143. /**
  144. * @dev Parse the type name out of the ERC-7739 contents type description. Supports both the implicit and explicit
  145. * modes.
  146. *
  147. * Following ERC-7739 specifications, a `contentsName` is considered invalid if it's empty or it contains
  148. * any of the following bytes , )\x00
  149. *
  150. * If the `contentsType` is invalid, this returns an empty string. Otherwise, the return string has non-zero
  151. * length.
  152. */
  153. function decodeContentsDescr(
  154. string calldata contentsDescr
  155. ) internal pure returns (string calldata contentsName, string calldata contentsType) {
  156. bytes calldata buffer = bytes(contentsDescr);
  157. if (buffer.length == 0) {
  158. // pass through (fail)
  159. } else if (buffer[buffer.length - 1] == bytes1(")")) {
  160. // Implicit mode: read contentsName from the beginning, and keep the complete descr
  161. for (uint256 i = 0; i < buffer.length; ++i) {
  162. bytes1 current = buffer[i];
  163. if (current == bytes1("(")) {
  164. // if name is empty - passthrough (fail)
  165. if (i == 0) break;
  166. // we found the end of the contentsName
  167. return (string(buffer[:i]), contentsDescr);
  168. } else if (_isForbiddenChar(current)) {
  169. // we found an invalid character (forbidden) - passthrough (fail)
  170. break;
  171. }
  172. }
  173. } else {
  174. // Explicit mode: read contentsName from the end, and remove it from the descr
  175. for (uint256 i = buffer.length; i > 0; --i) {
  176. bytes1 current = buffer[i - 1];
  177. if (current == bytes1(")")) {
  178. // we found the end of the contentsName
  179. return (string(buffer[i:]), string(buffer[:i]));
  180. } else if (_isForbiddenChar(current)) {
  181. // we found an invalid character (forbidden) - passthrough (fail)
  182. break;
  183. }
  184. }
  185. }
  186. return (Calldata.emptyString(), Calldata.emptyString());
  187. }
  188. function _isForbiddenChar(bytes1 char) private pure returns (bool) {
  189. return char == 0x00 || char == bytes1(" ") || char == bytes1(",") || char == bytes1("(") || char == bytes1(")");
  190. }
  191. }