|
@@ -0,0 +1,218 @@
|
|
|
+// SPDX-License-Identifier: MIT
|
|
|
+
|
|
|
+pragma solidity ^0.8.20;
|
|
|
+
|
|
|
+/**
|
|
|
+ * @dev Utilities to process https://ercs.ethereum.org/ERCS/erc-7739[ERC-7739] typed data signatures
|
|
|
+ * that are specific to an EIP-712 domain.
|
|
|
+ *
|
|
|
+ * This library provides methods to wrap, unwrap and operate over typed data signatures with a defensive
|
|
|
+ * rehashing mechanism that includes the application's {EIP712-_domainSeparatorV4} and preserves
|
|
|
+ * readability of the signed content using an EIP-712 nested approach.
|
|
|
+ *
|
|
|
+ * A smart contract domain can validate a signature for a typed data structure in two ways:
|
|
|
+ *
|
|
|
+ * - As an application validating a typed data signature. See {toNestedTypedDataHash}.
|
|
|
+ * - As a smart contract validating a raw message signature. See {toNestedPersonalSignHash}.
|
|
|
+ *
|
|
|
+ * NOTE: A provider for a smart contract wallet would need to return this signature as the
|
|
|
+ * result of a call to `personal_sign` or `eth_signTypedData`, and this may be unsupported by
|
|
|
+ * API clients that expect a return value of 129 bytes, or specifically the `r,s,v` parameters
|
|
|
+ * of an {ECDSA} signature, as is for example specified for {EIP712}.
|
|
|
+ */
|
|
|
+library ERC7739Utils {
|
|
|
+ /**
|
|
|
+ * @dev An EIP-712 type to represent "personal" signatures
|
|
|
+ * (i.e. mimic of `personal_sign` for smart contracts).
|
|
|
+ */
|
|
|
+ bytes32 private constant PERSONAL_SIGN_TYPEHASH = keccak256("PersonalSign(bytes prefixed)");
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @dev Error when the contents type is invalid. See {tryValidateContentsType}.
|
|
|
+ */
|
|
|
+ error InvalidContentsType();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @dev Nest a signature for a given EIP-712 type into a nested signature for the domain of the app.
|
|
|
+ *
|
|
|
+ * Counterpart of {decodeTypedDataSig} to extract the original signature and the nested components.
|
|
|
+ */
|
|
|
+ function encodeTypedDataSig(
|
|
|
+ bytes memory signature,
|
|
|
+ bytes32 appSeparator,
|
|
|
+ bytes32 contentsHash,
|
|
|
+ string memory contentsDescr
|
|
|
+ ) internal pure returns (bytes memory) {
|
|
|
+ return
|
|
|
+ abi.encodePacked(signature, appSeparator, contentsHash, contentsDescr, uint16(bytes(contentsDescr).length));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @dev Parses a nested signature into its components.
|
|
|
+ *
|
|
|
+ * Constructed as follows:
|
|
|
+ *
|
|
|
+ * `signature ‖ DOMAIN_SEPARATOR ‖ contentsHash ‖ contentsDescr ‖ uint16(contentsDescr.length)`
|
|
|
+ *
|
|
|
+ * - `signature` is the original signature for the nested struct hash that includes the "contents" hash
|
|
|
+ * - `DOMAIN_SEPARATOR` is the EIP-712 {EIP712-_domainSeparatorV4} of the smart contract verifying the signature
|
|
|
+ * - `contentsHash` is the hash of the underlying data structure or message
|
|
|
+ * - `contentsDescr` is a descriptor of the "contents" part of the the EIP-712 type of the nested signature
|
|
|
+ */
|
|
|
+ function decodeTypedDataSig(
|
|
|
+ bytes calldata encodedSignature
|
|
|
+ )
|
|
|
+ internal
|
|
|
+ pure
|
|
|
+ returns (bytes calldata signature, bytes32 appSeparator, bytes32 contentsHash, string calldata contentsDescr)
|
|
|
+ {
|
|
|
+ unchecked {
|
|
|
+ uint256 sigLength = encodedSignature.length;
|
|
|
+
|
|
|
+ if (sigLength < 4) return (_emptyCalldataBytes(), 0, 0, _emptyCalldataString());
|
|
|
+
|
|
|
+ uint256 contentsDescrEnd = sigLength - 2; // Last 2 bytes
|
|
|
+ uint256 contentsDescrLength = uint16(bytes2(encodedSignature[contentsDescrEnd:]));
|
|
|
+
|
|
|
+ if (contentsDescrLength + 64 > contentsDescrEnd)
|
|
|
+ return (_emptyCalldataBytes(), 0, 0, _emptyCalldataString());
|
|
|
+
|
|
|
+ uint256 contentsHashEnd = contentsDescrEnd - contentsDescrLength;
|
|
|
+ uint256 separatorEnd = contentsHashEnd - 32;
|
|
|
+ uint256 signatureEnd = separatorEnd - 32;
|
|
|
+
|
|
|
+ signature = encodedSignature[:signatureEnd];
|
|
|
+ appSeparator = bytes32(encodedSignature[signatureEnd:separatorEnd]);
|
|
|
+ contentsHash = bytes32(encodedSignature[separatorEnd:contentsHashEnd]);
|
|
|
+ contentsDescr = string(encodedSignature[contentsHashEnd:contentsDescrEnd]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @dev Nests an `ERC-191` digest into a `PersonalSign` EIP-712 struct, and return the corresponding struct hash.
|
|
|
+ * This struct hash must be combined with a domain separator, using {MessageHashUtils-toTypedDataHash} before
|
|
|
+ * being verified/recovered.
|
|
|
+ *
|
|
|
+ * This is used to simulates the `personal_sign` RPC method in the context of smart contracts.
|
|
|
+ */
|
|
|
+ function personalSignStructHash(bytes32 contents) internal pure returns (bytes32) {
|
|
|
+ return keccak256(abi.encode(PERSONAL_SIGN_TYPEHASH, contents));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @dev Nests an `EIP-712` hash (`contents`) into a `TypedDataSign` EIP-712 struct, and return the corresponding
|
|
|
+ * struct hash. This struct hash must be combined with a domain separator, using {MessageHashUtils-toTypedDataHash}
|
|
|
+ * before being verified/recovered.
|
|
|
+ */
|
|
|
+ function typedDataSignStructHash(
|
|
|
+ string calldata contentsTypeName,
|
|
|
+ string calldata contentsType,
|
|
|
+ bytes32 contentsHash,
|
|
|
+ bytes memory domainBytes
|
|
|
+ ) internal pure returns (bytes32 result) {
|
|
|
+ return
|
|
|
+ bytes(contentsTypeName).length == 0
|
|
|
+ ? bytes32(0)
|
|
|
+ : keccak256(
|
|
|
+ abi.encodePacked(typedDataSignTypehash(contentsTypeName, contentsType), contentsHash, domainBytes)
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @dev Variant of {typedDataSignStructHash-string-string-bytes32-string-bytes} that takes a content descriptor
|
|
|
+ * and decodes the `contentsTypeName` and `contentsType` out of it.
|
|
|
+ */
|
|
|
+ function typedDataSignStructHash(
|
|
|
+ string calldata contentsDescr,
|
|
|
+ bytes32 contentsHash,
|
|
|
+ bytes memory domainBytes
|
|
|
+ ) internal pure returns (bytes32 result) {
|
|
|
+ (string calldata contentsTypeName, string calldata contentsType) = decodeContentsDescr(contentsDescr);
|
|
|
+
|
|
|
+ return typedDataSignStructHash(contentsTypeName, contentsType, contentsHash, domainBytes);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @dev Compute the EIP-712 typehash of the `TypedDataSign` structure for a given type (and typename).
|
|
|
+ */
|
|
|
+ function typedDataSignTypehash(
|
|
|
+ string calldata contentsTypeName,
|
|
|
+ string calldata contentsType
|
|
|
+ ) internal pure returns (bytes32) {
|
|
|
+ return
|
|
|
+ keccak256(
|
|
|
+ abi.encodePacked(
|
|
|
+ "TypedDataSign(",
|
|
|
+ contentsTypeName,
|
|
|
+ " contents,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)",
|
|
|
+ contentsType
|
|
|
+ )
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @dev Parse the type name out of the ERC-7739 contents type description. Supports both the implicit and explicit
|
|
|
+ * modes.
|
|
|
+ *
|
|
|
+ * Following ERC-7739 specifications, a `contentsTypeName` is considered invalid if it's empty or it contains
|
|
|
+ * any of the following bytes , )\x00
|
|
|
+ *
|
|
|
+ * If the `contentsType` is invalid, this returns an empty string. Otherwise, the return string has non-zero
|
|
|
+ * length.
|
|
|
+ */
|
|
|
+ function decodeContentsDescr(
|
|
|
+ string calldata contentsDescr
|
|
|
+ ) internal pure returns (string calldata contentsTypeName, string calldata contentsType) {
|
|
|
+ bytes calldata buffer = bytes(contentsDescr);
|
|
|
+ if (buffer.length == 0) {
|
|
|
+ // pass through (fail)
|
|
|
+ } else if (buffer[buffer.length - 1] == bytes1(")")) {
|
|
|
+ // Implicit mode: read contentsTypeName for the beginning, and keep the complete descr
|
|
|
+ for (uint256 i = 0; i < buffer.length; ++i) {
|
|
|
+ bytes1 current = buffer[i];
|
|
|
+ if (current == bytes1("(")) {
|
|
|
+ // if name is empty - passthrough (fail)
|
|
|
+ if (i == 0) break;
|
|
|
+ // we found the end of the contentsTypeName
|
|
|
+ return (string(buffer[:i]), contentsDescr);
|
|
|
+ } else if (_isForbiddenChar(current)) {
|
|
|
+ // we found an invalid character (forbidden) - passthrough (fail)
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // Explicit mode: read contentsTypeName for the end, and remove it from the descr
|
|
|
+ for (uint256 i = buffer.length; i > 0; --i) {
|
|
|
+ bytes1 current = buffer[i - 1];
|
|
|
+ if (current == bytes1(")")) {
|
|
|
+ // we found the end of the contentsTypeName
|
|
|
+ return (string(buffer[i:]), string(buffer[:i]));
|
|
|
+ } else if (_isForbiddenChar(current)) {
|
|
|
+ // we found an invalid character (forbidden) - passthrough (fail)
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return (_emptyCalldataString(), _emptyCalldataString());
|
|
|
+ }
|
|
|
+
|
|
|
+ // slither-disable-next-line write-after-write
|
|
|
+ function _emptyCalldataBytes() private pure returns (bytes calldata result) {
|
|
|
+ assembly ("memory-safe") {
|
|
|
+ result.offset := 0
|
|
|
+ result.length := 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // slither-disable-next-line write-after-write
|
|
|
+ function _emptyCalldataString() private pure returns (string calldata result) {
|
|
|
+ assembly ("memory-safe") {
|
|
|
+ result.offset := 0
|
|
|
+ result.length := 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function _isForbiddenChar(bytes1 char) private pure returns (bool) {
|
|
|
+ return char == 0x00 || char == bytes1(" ") || char == bytes1(",") || char == bytes1("(") || char == bytes1(")");
|
|
|
+ }
|
|
|
+}
|