Skip to content
Closed
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
97 changes: 97 additions & 0 deletions contracts/utils/cryptography/ERC7803Utils.sol
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
)
);
Comment on lines +38 to +49
Copy link
Collaborator

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

Suggested change
return
signingDomainSeparators.length == 0
? MessageHashUtils.toTypedDataHash(verifyingDomainSeparator, structHash)
: MessageHashUtils.toSigningDomainHash(
signingDomainSeparators[0],
// TODO: Make iterative?
encodeForSigningDomains(
_splice(signingDomainSeparators, 1, signingDomainSeparators.length),
verifyingDomainSeparator,
structHash
)
);
bytes32 result = MessageHashUtils.toTypedDataHash(verifyingDomainSeparator, structHash);
for (uint256 i = signingDomainSeparators.length; i > 0; --i) {
result = MessageHashUtils.toSigningDomainHash(signingDomainSeparators[i-1], result);
}
return result;

}

/// @dev Checks if an authentication method ID corresponds to ECDSA.
function isECDSA(string memory methodId) internal pure returns (bool) {
return Strings.equal(methodId, "ECDSA");
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
return Strings.equal(methodId, "ECDSA");
return bytes(methodId).length == 5 && bytes5(bytes(methodId)) == 0x4543445341;

}

/// @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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)));
return (methodBytes.length > 4 && bytes4(methodBytes) == 0x4552432d)
? Strings.tryParseUint(string(Bytes.slice(methodBytes, 4)))
: (false, 0);

}

/**
* @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;
}
}
29 changes: 26 additions & 3 deletions contracts/utils/cryptography/MessageHashUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,34 @@ library MessageHashUtils {
* See {ECDSA-recover}.
*/
function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 digest) {
return _toTypedDataHash(hex"19_01", domainSeparator, structHash);
}

/**
* @dev Returns the keccak256 digest of an ERC-7803 signing domain data (ERC-191 version `0x02`).
*
* The digest is calculated by prefixing a `signingDomainSeparator` with `\x19\x02` and concatenating
* it with the `innerHash`, then hashing the result. This corresponds to the ERC-7803 signing domain
* encoding scheme for Account Abstraction.
*
* This function implements one step of the recursive encoding defined in ERC-7803:
* `"\x19\x02" ‖ signingDomainSeparator ‖ innerHash`
*
* See {ERC7803Utils-encodeForSigningDomains} for the complete recursive implementation.
*/
function toSigningDomainHash(
bytes32 signingDomainSeparator,
bytes32 innerHash
) internal pure returns (bytes32 digest) {
return _toTypedDataHash(hex"19_02", signingDomainSeparator, innerHash);
}

function _toTypedDataHash(bytes2 version, bytes32 separator, bytes32 hash) private pure returns (bytes32 digest) {
assembly ("memory-safe") {
let ptr := mload(0x40)
mstore(ptr, hex"19_01")
mstore(add(ptr, 0x02), domainSeparator)
mstore(add(ptr, 0x22), structHash)
mstore(ptr, version)
mstore(add(ptr, 0x02), separator)
mstore(add(ptr, 0x22), hash)
digest := keccak256(ptr, 0x42)
}
}
Expand Down
71 changes: 71 additions & 0 deletions contracts/utils/cryptography/signers/ERC7803.sol
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
);
}
}
Loading