-
Notifications
You must be signed in to change notification settings - Fork 12.4k
Add ERC7803Utils and ERC7803 #5722
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,97 @@ | ||||||||||||||||||||||||||
| // SPDX-License-Identifier: MIT | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| pragma solidity ^0.8.24; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import {MessageHashUtils} from "./MessageHashUtils.sol"; | ||||||||||||||||||||||||||
| import {Strings} from "../Strings.sol"; | ||||||||||||||||||||||||||
| import {Bytes} from "../Bytes.sol"; | ||||||||||||||||||||||||||
| import {Math} from "../math/Math.sol"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * @dev Utilities to process https://ercs.ethereum.org/ERCS/erc-7803[ERC-7803] signatures with signing domains | ||||||||||||||||||||||||||
| * for Account Abstraction. | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * This library provides methods to encode and decode ERC-7803 signatures that support signing domains | ||||||||||||||||||||||||||
| * and authentication method coordination. Signing domains prevent replay attacks when private keys | ||||||||||||||||||||||||||
| * are shared across smart contract accounts, while authentication methods allow dapps and wallets | ||||||||||||||||||||||||||
| * to coordinate on signature verification approaches. | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * The core functionality implements the recursive encoding scheme defined in ERC-7803: | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * * If signing domain separators exist: `"\x19\x02" ‖ first ‖ encodeForSigningDomains(others, verifyingDomainSeparator, message)` | ||||||||||||||||||||||||||
| * * If no signing domain separators: standard EIP-712 encoding | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| library ERC7803Utils { | ||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * @dev Encodes message for signing domains according to ERC-7803 specification. | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * This implements the recursive encoding scheme: | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * * If `signingDomainSeparators` is not empty: `"\x19\x02" ‖ first ‖ encodeForSigningDomains(others, verifyingDomainSeparator, message)` | ||||||||||||||||||||||||||
| * * If `signingDomainSeparators` is empty: standard EIP-712 encoding using {MessageHashUtils-toTypedDataHash} | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| function encodeForSigningDomains( | ||||||||||||||||||||||||||
| bytes32[] memory signingDomainSeparators, | ||||||||||||||||||||||||||
| bytes32 verifyingDomainSeparator, | ||||||||||||||||||||||||||
| bytes32 structHash | ||||||||||||||||||||||||||
| ) internal pure returns (bytes32) { | ||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||
| signingDomainSeparators.length == 0 | ||||||||||||||||||||||||||
| ? MessageHashUtils.toTypedDataHash(verifyingDomainSeparator, structHash) | ||||||||||||||||||||||||||
| : MessageHashUtils.toSigningDomainHash( | ||||||||||||||||||||||||||
| signingDomainSeparators[0], | ||||||||||||||||||||||||||
| // TODO: Make iterative? | ||||||||||||||||||||||||||
| encodeForSigningDomains( | ||||||||||||||||||||||||||
| _splice(signingDomainSeparators, 1, signingDomainSeparators.length), | ||||||||||||||||||||||||||
| verifyingDomainSeparator, | ||||||||||||||||||||||||||
| structHash | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /// @dev Checks if an authentication method ID corresponds to ECDSA. | ||||||||||||||||||||||||||
| function isECDSA(string memory methodId) internal pure returns (bool) { | ||||||||||||||||||||||||||
| return Strings.equal(methodId, "ECDSA"); | ||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When we know the string, and when the length is <=32, it is cheaper (but arguably less readable) to compare things directly
Suggested change
|
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /// @dev Checks if an authentication method ID corresponds to an ERC standard. | ||||||||||||||||||||||||||
| function isERC(string memory methodId) internal pure returns (bool, uint256) { | ||||||||||||||||||||||||||
| bytes memory methodBytes = bytes(methodId); | ||||||||||||||||||||||||||
| if (methodBytes.length < 4) return (false, 0); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Check if it starts with "ERC-" | ||||||||||||||||||||||||||
| if (!(methodBytes[0] == "E" && methodBytes[1] == "R" && methodBytes[2] == "C" && methodBytes[3] == "-")) { | ||||||||||||||||||||||||||
| return (false, 0); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Extract and validate the ERC number | ||||||||||||||||||||||||||
| return Strings.tryParseUint(string(Bytes.slice(methodBytes, 4))); | ||||||||||||||||||||||||||
|
Comment on lines
+60
to
+68
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * @dev Splices a slice of a bytes32 array. Avoids expanding memory. | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * Replicates https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's `Array.splice`] | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * NOTE: Clears the array if `start` is greater than `end`. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| function _splice(bytes32[] memory data, uint256 start, uint256 end) private pure returns (bytes32[] memory) { | ||||||||||||||||||||||||||
| // sanitize | ||||||||||||||||||||||||||
| uint256 length = data.length; | ||||||||||||||||||||||||||
| end = Math.min(end, length); | ||||||||||||||||||||||||||
| start = Math.min(start, end); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (start != 0) { | ||||||||||||||||||||||||||
| // allocate and copy | ||||||||||||||||||||||||||
| for (uint256 i = start; i < end; i++) { | ||||||||||||||||||||||||||
| data[i - start] = data[i]; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| assembly ("memory-safe") { | ||||||||||||||||||||||||||
| mstore(data, sub(end, start)) // Reset the length of the array. Can't overflow because `end` is less than `data.length`. | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return data; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| pragma solidity ^0.8.20; | ||
|
|
||
| import {AbstractSigner} from "./AbstractSigner.sol"; | ||
| import {EIP712} from "../EIP712.sol"; | ||
| import {ERC7803Utils} from "../ERC7803Utils.sol"; | ||
| import {IERC1271} from "../../../interfaces/IERC1271.sol"; | ||
| import {MessageHashUtils} from "../MessageHashUtils.sol"; | ||
|
|
||
| /** | ||
| * @dev Validates signatures using ERC-7803 signing domains for Account Abstraction. | ||
| * | ||
| * This contract implements the ERC-7803 specification which provides improvements for EIP-712 signatures | ||
| * to better support smart contract accounts by: | ||
| * | ||
| * 1. Introducing signing domains to prevent replay attacks when private keys are shared across accounts | ||
| * 2. Allowing dapps and wallets to coordinate on authentication methods | ||
| * | ||
| * The recursive encoding scheme prevents signature replay across different account hierarchies while | ||
| * maintaining compatibility with existing EIP-712 infrastructure. | ||
| * | ||
| * NOTE: xref:api:utils/cryptography#EIP712[EIP-712] uses xref:api:utils/cryptography#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 ERC7803 is AbstractSigner, EIP712, IERC1271 { | ||
| using ERC7803Utils for *; | ||
| using MessageHashUtils for bytes32; | ||
|
|
||
| /** | ||
| * @dev Attempts validating the signature using ERC-7803 signing domains and authentication methods. | ||
| * | ||
| * The validation process follows these steps: | ||
| * | ||
| * 1. Try validation with signing domains if provided | ||
| * 2. Fall back to standard EIP-712 validation | ||
| * 3. Apply authentication methods in the specified order | ||
| */ | ||
| function isValidSignature(bytes32 hash, bytes calldata signature) public view virtual returns (bytes4 result) { | ||
| // For the hash `0x7803780378037803780378037803780378037803780378037803780378037803` and an empty signature, | ||
| // we return the magic value `0x78030001` 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-7803. | ||
| return | ||
| (_isValidERC7803Signature(hash, signature) || _rawSignatureValidation(hash, signature)) | ||
| ? IERC1271.isValidSignature.selector | ||
| : (hash == 0x7803780378037803780378037803780378037803780378037803780378037803 && signature.length == 0) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AFAIK, ERC-7803 doesn't document anything like this |
||
| ? bytes4(0x78030001) | ||
| : bytes4(0xffffffff); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Validates signature using ERC-7803 signing domains. | ||
| * This function decodes the signature to extract signing domain information and validates accordingly. | ||
| * | ||
| * The signature is encoded as: `abi.encodePacked(uint16(bytesLength),bytes,abi.encode(bytes32[]))` | ||
| */ | ||
| function _isValidERC7803Signature(bytes32 hash, bytes calldata signature) internal view returns (bool) { | ||
| uint16 bytesLength = uint16(bytes2(signature[0:2])); | ||
| bytes calldata actualSignature = signature[2:bytesLength + 2]; | ||
| bytes calldata encodedData = signature[bytesLength + 2:]; | ||
|
|
||
| return | ||
| encodedData.length > 0 && | ||
| _rawSignatureValidation( | ||
| abi.decode(encodedData, (bytes32[])).encodeForSigningDomains(_domainSeparatorV4(), hash), | ||
| actualSignature | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a fan of the recurtion here, particularly because it require a _splice that is memory intensive. A simple for loop should work just fine