Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lucky-donuts-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`ERC7739`: An abstract contract to validate signatures following the rehashing scheme from `ERC7739Utils`.
5 changes: 5 additions & 0 deletions .changeset/proud-tables-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`ERC7739Utils`: Add a library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on the ERC-7739.
28 changes: 28 additions & 0 deletions contracts/mocks/utils/cryptography/ERC7739Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {ECDSA} from "../../../utils/cryptography/ECDSA.sol";
import {EIP712} from "../../../utils/cryptography/EIP712.sol";
import {ERC7739} from "../../../utils/cryptography/ERC7739.sol";
import {AbstractSigner} from "../../../utils/cryptography/AbstractSigner.sol";

contract ERC7739ECDSAMock is AbstractSigner, ERC7739 {
address private _signer;

constructor(address signerAddr) EIP712("ERC7739ECDSA", "1") {
_signer = signerAddr;
}

function signer() public view virtual returns (address) {
return _signer;
}

function _rawSignatureValidation(
bytes32 hash,
bytes calldata signature
) internal view virtual override returns (bool) {
(address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature);
return signer() == recovered && err == ECDSA.RecoverError.NoError;
}
}
13 changes: 12 additions & 1 deletion contracts/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ Miscellaneous contracts and libraries containing utility functions you can use t
* {Comparators}: A library that contains comparator functions to use with the {Heap} library.
* {CAIP2}, {CAIP10}: Libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers.
* {Blockhash}: A library for accessing historical block hashes beyond the standard 256 block limit utilizing EIP-2935's historical blockhash functionality.

* {AbstractSigner}: Abstract contract for internal signature validation in smart contracts.
* {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from `ERC7739Utils`.
* {ERC7739Utils}: Utilities library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on ERC-7739.

[NOTE]
====
Because Solidity does not support generic types, {EnumerableMap} and {EnumerableSet} are specialized to a limited number of key-value types.
Expand Down Expand Up @@ -78,6 +81,14 @@ Because Solidity does not support generic types, {EnumerableMap} and {Enumerable

{{MerkleProof}}

{{ERC7739}}

{{ERC7739Utils}}

=== Abstract Signers

{{AbstractSigner}}

== Security

{{ReentrancyGuard}}
Expand Down
22 changes: 22 additions & 0 deletions contracts/utils/cryptography/AbstractSigner.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

/**
* @dev Abstract contract for signature validation.
*
* Developers must implement {_rawSignatureValidation} and use it as the lowest-level signature validation mechanism.
*
* @custom:stateless
*/
abstract contract AbstractSigner {
/**
* @dev Signature validation algorithm.
*
* WARNING: Implementing a signature validation algorithm is a security-sensitive operation as it involves
* cryptographic verification. It is important to review and test thoroughly before deployment. Consider
* using one of the signature verification libraries (xref:api:utils#ECDSA[ECDSA], xref:api:utils#P256[P256]
* or xref:api:utils#RSA[RSA]).
*/
function _rawSignatureValidation(bytes32 hash, bytes calldata signature) internal view virtual returns (bool);
}
98 changes: 98 additions & 0 deletions contracts/utils/cryptography/ERC7739.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {IERC1271} from "../../interfaces/IERC1271.sol";
import {EIP712} from "../cryptography/EIP712.sol";
import {MessageHashUtils} from "../cryptography/MessageHashUtils.sol";
import {ShortStrings} from "../ShortStrings.sol";
import {AbstractSigner} from "./AbstractSigner.sol";
import {ERC7739Utils} from "./ERC7739Utils.sol";

/**
* @dev Validates signatures wrapping the message hash in a nested EIP712 type. See {ERC7739Utils}.
*
* Linking the signature to the EIP-712 domain separator is a security measure to prevent signature replay across different
* EIP-712 domains (e.g. a single offchain owner of multiple contracts).
*
* This contract requires implementing the {_rawSignatureValidation} function, which passes the wrapped message hash,
* which may be either an typed data or a personal sign nested type.
*
* NOTE: xref:api:utils#EIP712[EIP-712] uses xref:api:utils#ShortStrings[ShortStrings] to optimize gas
* costs for short strings (up to 31 characters). Consider that strings longer than that will use storage,
* which may limit the ability of the signer to be used within the ERC-4337 validation phase (due to
* https://eips.ethereum.org/EIPS/eip-7562#storage-rules[ERC-7562 storage access rules]).
*/
abstract contract ERC7739 is AbstractSigner, EIP712, IERC1271 {
using ERC7739Utils for *;
using MessageHashUtils for bytes32;

/**
* @dev Attempts validating the signature in a nested EIP-712 type.
*
* A nested EIP-712 type might be presented in 2 different ways:
*
* - As a nested EIP-712 typed data
* - As a _personal_ signature (an EIP-712 mimic of the `eth_personalSign` for a smart contract)
*/
function isValidSignature(bytes32 hash, bytes calldata signature) public view virtual returns (bytes4 result) {
// For the hash `0x7739773977397739773977397739773977397739773977397739773977397739` and an empty signature,
// we return the magic value `0x77390001` as it's assumed impossible to find a preimage for it that can be used
// maliciously. Useful for simulation purposes and to validate whether the contract supports ERC-7739.
return
(_isValidNestedTypedDataSignature(hash, signature) || _isValidNestedPersonalSignSignature(hash, signature))
? IERC1271.isValidSignature.selector
: (hash == 0x7739773977397739773977397739773977397739773977397739773977397739 && signature.length == 0)
? bytes4(0x77390001)
: bytes4(0xffffffff);
}

/**
* @dev Nested personal signature verification.
*/
function _isValidNestedPersonalSignSignature(bytes32 hash, bytes calldata signature) private view returns (bool) {
return _rawSignatureValidation(_domainSeparatorV4().toTypedDataHash(hash.personalSignStructHash()), signature);
}

/**
* @dev Nested EIP-712 typed data verification.
*/
function _isValidNestedTypedDataSignature(
bytes32 hash,
bytes calldata encodedSignature
) private view returns (bool) {
// decode signature
(
bytes calldata signature,
bytes32 appSeparator,
bytes32 contentsHash,
string calldata contentsDescr
) = encodedSignature.decodeTypedDataSig();

(
,
string memory name,
string memory version,
uint256 chainId,
address verifyingContract,
bytes32 salt,

) = eip712Domain();

// Check that contentHash and separator are correct
// Rebuild nested hash
return
hash == appSeparator.toTypedDataHash(contentsHash) &&
bytes(contentsDescr).length != 0 &&
_rawSignatureValidation(
appSeparator.toTypedDataHash(
ERC7739Utils.typedDataSignStructHash(
contentsDescr,
contentsHash,
abi.encode(keccak256(bytes(name)), keccak256(bytes(version)), chainId, verifyingContract, salt)
)
),
signature
);
}
}
206 changes: 206 additions & 0 deletions contracts/utils/cryptography/ERC7739Utils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Calldata} from "../Calldata.sol";

/**
* @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 xref:api:utils#EIP712-_domainSeparatorV4[EIP-712]
* 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 {typedDataSignStructHash}.
* - As a smart contract validating a raw message signature. See {personalSignStructHash}.
*
* 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 xref:api:utils#ECDSA[ECDSA] signature, as is for example specified for
* xref:api:utils#EIP712[EIP-712].
*/
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 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 ‖ APP_DOMAIN_SEPARATOR ‖ contentsHash ‖ contentsDescr ‖ uint16(contentsDescr.length)`
*
* - `signature` is the signature for the (ERC-7739) nested struct hash. This signature indirectly signs over the
* original "contents" hash (from the app) and the account's domain separator.
* - `APP_DOMAIN_SEPARATOR` is the EIP-712 {EIP712-_domainSeparatorV4} of the application smart contract that is
* requesting the signature verification (though ERC-1271).
* - `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.
*
* NOTE: This function returns empty if the input format is invalid instead of reverting.
* data instead.
*/
function decodeTypedDataSig(
bytes calldata encodedSignature
)
internal
pure
returns (bytes calldata signature, bytes32 appSeparator, bytes32 contentsHash, string calldata contentsDescr)
{
unchecked {
uint256 sigLength = encodedSignature.length;

// 66 bytes = contentsDescrLength (2 bytes) + contentsHash (32 bytes) + APP_DOMAIN_SEPARATOR (32 bytes).
if (sigLength < 66) return (Calldata.emptyBytes(), 0, 0, Calldata.emptyString());

uint256 contentsDescrEnd = sigLength - 2; // Last 2 bytes
uint256 contentsDescrLength = uint16(bytes2(encodedSignature[contentsDescrEnd:]));

// Check for space for `contentsDescr` in addition to the 66 bytes documented above
if (sigLength < 66 + contentsDescrLength) return (Calldata.emptyBytes(), 0, 0, Calldata.emptyString());

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 returns 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 returns 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 contentsName,
string calldata contentsType,
bytes32 contentsHash,
bytes memory domainBytes
) internal pure returns (bytes32 result) {
return
bytes(contentsName).length == 0
? bytes32(0)
: keccak256(
abi.encodePacked(typedDataSignTypehash(contentsName, contentsType), contentsHash, domainBytes)
);
}

/**
* @dev Variant of {typedDataSignStructHash-string-string-bytes32-bytes} that takes a content descriptor
* and decodes the `contentsName` and `contentsType` out of it.
*/
function typedDataSignStructHash(
string calldata contentsDescr,
bytes32 contentsHash,
bytes memory domainBytes
) internal pure returns (bytes32 result) {
(string calldata contentsName, string calldata contentsType) = decodeContentsDescr(contentsDescr);

return typedDataSignStructHash(contentsName, contentsType, contentsHash, domainBytes);
}

/**
* @dev Compute the EIP-712 typehash of the `TypedDataSign` structure for a given type (and typename).
*/
function typedDataSignTypehash(
string calldata contentsName,
string calldata contentsType
) internal pure returns (bytes32) {
return
keccak256(
abi.encodePacked(
"TypedDataSign(",
contentsName,
" 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 `contentsName` 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 contentsName, 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 contentsName from 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 contentsName
return (string(buffer[:i]), contentsDescr);
} else if (_isForbiddenChar(current)) {
// we found an invalid character (forbidden) - passthrough (fail)
break;
}
}
} else {
// Explicit mode: read contentsName from 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 contentsName
return (string(buffer[i:]), string(buffer[:i]));
} else if (_isForbiddenChar(current)) {
// we found an invalid character (forbidden) - passthrough (fail)
break;
}
}
}
return (Calldata.emptyString(), Calldata.emptyString());
}

function _isForbiddenChar(bytes1 char) private pure returns (bool) {
return char == 0x00 || char == bytes1(" ") || char == bytes1(",") || char == bytes1("(") || char == bytes1(")");
}
}
Loading