draft-ERC7739Utils.sol 9.2 KB

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