-
Notifications
You must be signed in to change notification settings - Fork 12.3k
Add ERC7739 and ERC7739Utils #5664
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
1c97739
Test ethers 6.13.6-beta.1
ernestognw 6b1bbd8
up
ernestognw 19fe4c5
up
ernestognw c39d5f5
Tweak workflows
ernestognw 54f632a
Use Solidity 0.8.27 as default and set default EVM to prague
ernestognw 1f4992f
Add ERC7739 and ERC7379Utils
ernestognw 58c794e
Adjust ERC2771Forwarder gas to avoid GasFloorMoreThanGasLimit
ernestognw ca17f9a
Merge branch 'test/ethers-6.13.6-beta.1' into feature/erc7739
ernestognw b4f0f48
Add docs
ernestognw 3ccccd1
Merge branch 'master' into feature/erc7739
ernestognw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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(")"); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.