From 6c96bb77c4cfcfd154d792a0ef11908dbc920ef8 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 20 Oct 2025 17:11:12 -0600 Subject: [PATCH] Add ERC6492 signature validation --- .../utils/cryptography/SignatureChecker.sol | 231 +++++++++++++++++- 1 file changed, 230 insertions(+), 1 deletion(-) diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index 036be785eb4..798f99da881 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -7,19 +7,26 @@ import {ECDSA} from "./ECDSA.sol"; import {IERC1271} from "../../interfaces/IERC1271.sol"; import {IERC7913SignatureVerifier} from "../../interfaces/IERC7913.sol"; import {Bytes} from "../Bytes.sol"; +import {LowLevelCall} from "../LowLevelCall.sol"; +import {RelayedCall} from "../RelayedCall.sol"; +import {Calldata} from "../Calldata.sol"; /** * @dev Signature verification helper that can be used instead of `ECDSA.recover` to seamlessly support: * * * ECDSA signatures from externally owned accounts (EOAs) * * ERC-1271 signatures from smart contract wallets like Argent and Safe Wallet (previously Gnosis Safe) + * * ERC-6492 signatures from smart contracts that have not been deployed yet * * ERC-7913 signatures from keys that do not have an Ethereum address of their own * - * See https://eips.ethereum.org/EIPS/eip-1271[ERC-1271] and https://eips.ethereum.org/EIPS/eip-7913[ERC-7913]. + * See https://eips.ethereum.org/EIPS/eip-1271[ERC-1271], https://eips.ethereum.org/EIPS/eip-6492[ERC-6492], + * and https://eips.ethereum.org/EIPS/eip-7913[ERC-7913]. */ library SignatureChecker { using Bytes for bytes; + bytes32 private constant ERC6492_SUFFIX = 0x6492649264926492649264926492649264926492649264926492649264926492; + /** * @dev Checks if a signature is valid for a given signer and data hash. If the signer has code, the * signature is validated against it using ERC-1271, otherwise it's validated using `ECDSA.recover`. @@ -27,6 +34,9 @@ library SignatureChecker { * NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus * change through time. It could return true at block N and false at block N+1 (or the opposite). * + * NOTE: For ERC-6492 signature validation (signatures from contracts that have not been deployed yet), + * see {isValidERC6492SignatureNow}. + * * NOTE: For an extended version of this function that supports ERC-7913 signatures, see {isValidSignatureNow-bytes-bytes32-bytes-}. */ function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { @@ -87,6 +97,122 @@ library SignatureChecker { } } + function isValidERC6492SignatureNowAllowSideEffects( + address signer, + bytes32 hash, + bytes memory signature + ) internal returns (bool) { + bool hasCode = signer.code.length > 0; + bytes32 suffix; + assembly ("memory-safe") { + // Extracts the suffix word without allocating extra memory + suffix := mload(add(signature, mload(signature))) + } + bool isValid; + + // Behaves as a switch statement + while (true) { + // Case 1: Standard ERC-1271 signature validation + if (suffix != ERC6492_SUFFIX) { + if (hasCode) isValid = isValidERC1271SignatureNow(signer, hash, signature); + break; + } + + // Case 2: ERC-6492 signature validation with a contract already deployed + // Assumes the signature is always encoded as (factoryOrPrepareTo, factoryOrPrepareCalldata, signature, suffix) + // since the contract might had been deployed before the signature was submitted. + bool decodable; + address factoryOrPrepareTo; + bytes memory factoryOrPrepareCalldata; + (decodable, factoryOrPrepareTo, factoryOrPrepareCalldata, signature) = _tryDecodeERC6492Signature( + signature.splice(0, signature.length - 32) // Remove the suffix + ); + if (!decodable) return false; // Suffixed. Can't decode. Just return false. + if (hasCode) { + isValid = isValidERC1271SignatureNow(signer, hash, signature); + if (isValid) break; + } + + // Case 3: ERC-6492 signature validation with a contract not yet deployed or preparation failed + // Either has no code or the signature was invalid. We need to try to deploy/prepare the contract. + if ( + LowLevelCall.callNoReturn( + // Use a dedicated relayer for ERC-6492 signatures so that the caller + // can't be arbitrarily used if it has special permissions on a target contract + RelayedCall.getRelayer(ERC6492_SUFFIX), + abi.encodePacked(factoryOrPrepareTo, factoryOrPrepareCalldata) + ) + ) { + isValid = isValidERC1271SignatureNow(signer, hash, signature); + } // Do not attempt validation if the call failed + break; + } + + // Fallback to ECDSA signature validation + if (!hasCode && !isValid) { + (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature); + isValid = err == ECDSA.RecoverError.NoError && recovered == signer; + } + + return isValid; + } + + function isValidERC6492SignatureNowAllowSideEffectsCalldata( + address signer, + bytes32 hash, + bytes calldata signature + ) internal returns (bool) { + bool hasCode = signer.code.length > 0; + bytes32 suffix = bytes32(signature[signature.length - 32:]); + bool isValid; + + // Behaves as a switch statement + while (true) { + // Case 1: Standard ERC-1271 signature validation + if (suffix != ERC6492_SUFFIX) { + if (hasCode) isValid = isValidERC1271SignatureNow(signer, hash, signature); + break; + } + + // Case 2: ERC-6492 signature validation with a contract already deployed + // Assumes the signature is always encoded as (factoryOrPrepareTo, factoryOrPrepareCalldata, signature, suffix) + // since the contract might had been deployed before the signature was submitted. + bool decodable; + address factoryOrPrepareTo; + bytes calldata factoryOrPrepareCalldata; + (decodable, factoryOrPrepareTo, factoryOrPrepareCalldata, signature) = _tryDecodeERC6492SignatureCalldata( + signature[0:signature.length - 32] // Avoid the suffix + ); + if (!decodable) return false; // Suffixed. Can't decode. Just return false. + if (hasCode) { + isValid = isValidERC1271SignatureNow(signer, hash, signature); + if (isValid) break; + } + + // Case 3: ERC-6492 signature validation with a contract not yet deployed or preparation failed + // Either has no code or the signature was invalid. We need to try to deploy/prepare the contract. + if ( + LowLevelCall.callNoReturn( + // Use a dedicated relayer for ERC-6492 signatures so that the caller + // can't be arbitrarily used if it has special permissions on a target contract + RelayedCall.getRelayer(ERC6492_SUFFIX), + abi.encodePacked(factoryOrPrepareTo, factoryOrPrepareCalldata) + ) + ) { + isValid = isValidERC1271SignatureNow(signer, hash, signature); + } // Do not attempt validation if the call failed + break; + } + + // Fallback to ECDSA signature validation + if (!hasCode && !isValid) { + (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature); + isValid = err == ECDSA.RecoverError.NoError && recovered == signer; + } + + return isValid; + } + /** * @dev Verifies a signature for a given ERC-7913 signer and hash. * @@ -161,4 +287,107 @@ library SignatureChecker { return true; } + + function _tryDecodeERC6492Signature( + bytes memory signature + ) + private + pure + returns ( + bool decodable, + address factoryOrPrepareTo, + bytes memory factoryOrPrepareCalldata, + bytes memory innerSignature + ) + { + // Minimum length of a valid ERC-6492 signature is 160 bytes + // [32 bytes: address] + // [32 bytes: factoryOrPrepareCalldataOffset] + // [32 bytes: signatureOffset] + // [32 bytes: factoryOrPrepareCalldataLength] + // [32 bytes: signatureLength] + if (signature.length < 160) return (false, address(0), bytes(""), bytes("")); + + uint256 factoryOrPrepareCalldataOffset; + uint256 signatureOffset; + bytes32 ptr; + assembly ("memory-safe") { + ptr := add(signature, 0x20) + factoryOrPrepareTo := mload(ptr) + factoryOrPrepareCalldataOffset := mload(add(ptr, 0x20)) + signatureOffset := mload(add(ptr, 0x40)) + } + if (factoryOrPrepareCalldataOffset > signature.length || signatureOffset > signature.length) + // Validate bounds + return (false, address(0), bytes(""), bytes("")); + + uint256 factoryOrPrepareCalldataLength; + uint256 signatureLength; + assembly ("memory-safe") { + factoryOrPrepareCalldataLength := mload(add(ptr, factoryOrPrepareCalldataOffset)) + signatureLength := mload(add(ptr, signatureOffset)) + } + if ( + factoryOrPrepareCalldataOffset + factoryOrPrepareCalldataLength > signature.length - 32 || + signatureOffset + signatureLength > signature.length - 32 + ) + // Validate bounds + return (false, address(0), bytes(""), bytes("")); + + return ( + true, + factoryOrPrepareTo, + signature.slice( + factoryOrPrepareCalldataOffset, + factoryOrPrepareCalldataOffset + factoryOrPrepareCalldataLength + ), + signature.slice(signatureOffset, signatureOffset + signatureLength) + ); + } + + function _tryDecodeERC6492SignatureCalldata( + bytes calldata signature + ) + private + pure + returns ( + bool decodable, + address factoryOrPrepareTo, + bytes calldata factoryOrPrepareCalldata, + bytes calldata innerSignature + ) + { + // Minimum length of a valid ERC-6492 signature is 160 bytes + // [32 bytes: address] + // [32 bytes: factoryOrPrepareCalldataOffset] + // [32 bytes: signatureOffset] + // [32 bytes: factoryOrPrepareCalldataLength] + // [32 bytes: signatureLength] + if (signature.length < 160) return (false, address(0), Calldata.emptyBytes(), Calldata.emptyBytes()); + + factoryOrPrepareTo = address(bytes20(signature[12:32])); + uint256 factoryOrPrepareCalldataOffset = uint256(bytes32(signature[32:64])); + uint256 signatureOffset = uint256(bytes32(signature[64:96])); + if (factoryOrPrepareCalldataOffset > signature.length || signatureOffset > signature.length) + // Validate bounds + return (false, address(0), Calldata.emptyBytes(), Calldata.emptyBytes()); + + uint256 factoryOrPrepareCalldataLength = uint256( + bytes32(signature[factoryOrPrepareCalldataOffset:factoryOrPrepareCalldataOffset + 32]) + ); + uint256 signatureLength = uint256(bytes32(signature[signatureOffset:signatureOffset + 32])); + if ( + factoryOrPrepareCalldataOffset + factoryOrPrepareCalldataLength > signature.length - 32 || + signatureOffset + signatureLength > signature.length - 32 + ) + // Validate bounds + return (false, address(0), Calldata.emptyBytes(), Calldata.emptyBytes()); + + return ( + true, + factoryOrPrepareTo, + signature[factoryOrPrepareCalldataOffset:factoryOrPrepareCalldataOffset + factoryOrPrepareCalldataLength], + signature[signatureOffset:signatureOffset + signatureLength] + ); + } }