-
Notifications
You must be signed in to change notification settings - Fork 7
fix: Implement custom 6492 compatible signMessage #198
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
Conversation
WalkthroughAdds EIP-6492 signMessage support to EtherspotTransactionKit: new public signMessage method and internal helper to build an EIP-6492-formatted signature, with automatic EIP-7702 authorization creation when using delegatedEoa wallet mode. Changes
Sequence DiagramsequenceDiagram
participant Client
participant ETK as EtherspotTransactionKit
participant Owner as Owner Account
participant Auth as EIP-7702 Authorization Service
participant Viem as Viem Utilities
participant Signer as Signer Client
Client->>ETK: signMessage(message, chainId?)
ETK->>ETK: validate delegatedEoa mode
ETK->>Owner: resolve owner account (LocalAccount)
alt owner not delegated
ETK->>Auth: create EIP-7702 authorization
Auth-->>ETK: authorization token / deployment info
else owner already delegated
ETK->>Auth: fetch existing authorization
Auth-->>ETK: deployment info
end
ETK->>Viem: encode deployment data (toHex, toRlp, encodeAbiParameters)
Viem-->>ETK: deployment bytes
ETK->>Signer: sign (personal_sign / EIP-191) using owner account
Signer-->>ETK: raw signature
ETK->>ETK: format 0x6492 + signature + deployment data
ETK-->>Client: 0x6492-formatted signature
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Yooo! You forgot to bump the version in package.json! |
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.
Actionable comments posted: 2
🧹 Nitpick comments (2)
lib/TransactionKit.ts (1)
7-12: Double‑check EIP‑6492/EIP‑7702 encoding details andSignAuthorizationReturnTypeusage insignMessage.The overall flow (delegatedEoa‑only, owner/wallet/bundler clients, automatic authorization, RLP deployment payload, and
0x6492<signature><deployment_data>framing) is coherent, but there are a few interop details worth validating:
authorization.datalikely not present on viem’sSignAuthorizationReturnType.
The type exported fromviem/accountscurrently exposes fields likechainId,contractAddress(oraddressin some stacks),nonce,r,s,yParity, but not adatafield. As written:const authorizationData = authorization.data || '0x';will resolve to
'0x'for viem’sSignedAuthorization, so yourdeploymentDatawill not actually include any bytes derived from the EIP‑7702 authorization itself. If your verifier expects the deployment payload to carry the authorization tuple (e.g. RLP of[chainId, delegateAddress, nonce, yParity, r, s]or similar), it would be safer to derive that explicitly fromauthorizationinstead of relying on.data.Non‑canonical ERC‑6492 framing (prefix vs suffix).
The ERC‑6492 spec defines a wrapper that ABI‑encodes(factory, factoryCalldata, innerSignature)and appends a 32‑byte magic suffix0x6492…6492for detection; most generic validators (e.g. Solady’sisValidERC6492SignatureNow*, Coinbase’sPublicERC6492Validator) look for that suffix and ABI layout. This implementation instead:
- Prepends a short magic prefix
0x6492, and- Concatenates
<signatureBytes><deploymentBytes>.If your downstream verifiers are custom and built to match this
0x6492<signature><rlpTx>format, that’s fine, but it will not be drop‑in compatible with off‑the‑shelf ERC‑6492 validators. In that case, it may be worth calling this out explicitly in the public docs/API as a TransactionKit‑specific 6492 variant.Message typing for hex inputs.
walletClient.signMessageis invoked as:const signature = await walletClient.signMessage({ account: owner, message: message as string, });which means hex‑looking inputs like
'0x68656c6c6f'are signed as UTF‑8 strings, not as raw bytes (message: { raw: hex }in viem terms). That’s consistent if you conceptually treat “hex messages” as just string payloads, but if callers expect byte‑level signing for0xmessages you may want to branch ontypeof message === 'string' && message.startsWith('0x')and passmessage: { raw: message }instead.None of this blocks the PR if your current consumers and tests are built around this encoding, but it would be good to confirm the intended interoperability story with canonical ERC‑6492/EIP‑7702 tooling and adjust either the implementation (authorization payload, framing) or the documentation accordingly.
Also applies to: 664-831
example/src/signMessage-example.ts (1)
23-36: Tighten env validation in the example to avoid running with an invalid default key.The example is great for demonstrating the flow, but right now it will happily try to talk to the network even when
PRIVATE_KEYis left at the placeholder:const PRIVATE_KEY = process.env.REACT_APP_DEMO_WALLET_PK || process.env.PRIVATE_KEY || '0x' + '0'.repeat(64); ... if (PRIVATE_KEY === '0x' + '0'.repeat(64)) { log('\n⚠️ WARNING: Using default private key. Set PRIVATE_KEY in .env file!', 'yellow'); }In viem/EIP‑7702 flows that key is effectively invalid and will typically result in “Invalid private key” or signing failures further down.
For a smoother DX you might want to fail fast instead of proceeding:
-const PRIVATE_KEY = process.env.REACT_APP_DEMO_WALLET_PK || process.env.PRIVATE_KEY || '0x' + '0'.repeat(64); +const PRIVATE_KEY = + process.env.REACT_APP_DEMO_WALLET_PK || process.env.PRIVATE_KEY || ''; @@ - // Validate private key - if (PRIVATE_KEY === '0x' + '0'.repeat(64)) { - log('\n⚠️ WARNING: Using default private key. Set PRIVATE_KEY in .env file!', 'yellow'); - } + // Validate private key + if (!PRIVATE_KEY || PRIVATE_KEY.length !== 66 || !PRIVATE_KEY.startsWith('0x')) { + log('\nPrivate key is not set or has an invalid format. Set PRIVATE_KEY/REACT_APP_DEMO_WALLET_PK in .env.', 'red'); + process.exit(1); + }This keeps the example focused on the signing behavior instead of surfacing lower‑level cryptography errors when someone runs it without configuring a real test key.
Also applies to: 359-385
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
example/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (10)
CHANGELOG.md(1 hunks)__tests__/EtherspotTransactionKit.test.ts(2 hunks)example/.env.example(1 hunks)example/README.md(1 hunks)example/package.json(2 hunks)example/src/signMessage-example.ts(1 hunks)example/validate-example.js(1 hunks)lib/TransactionKit.ts(2 hunks)lib/interfaces/index.ts(1 hunks)package.json(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
lib/TransactionKit.ts (1)
lib/utils/index.ts (1)
log(70-75)
example/src/signMessage-example.ts (2)
lib/utils/index.ts (1)
log(70-75)lib/TransactionKit.ts (1)
TransactionKit(4666-4670)
🪛 dotenv-linter (4.0.0)
example/.env.example
[warning] 3-3: [UnorderedKey] The REACT_APP_BUNDLER_URL key should go before the REACT_APP_DEMO_WALLET_PK key
(UnorderedKey)
[warning] 5-5: [UnorderedKey] The REACT_APP_CHAIN_ID key should go before the REACT_APP_DEMO_WALLET_PK key
(UnorderedKey)
🪛 LanguageTool
CHANGELOG.md
[grammar] ~7-~7: Use a hyphen to join words.
Context: ...atures that can be validated by EIP-6492 compatible validators. The method wraps ...
(QB_NEW_EN_HYPHEN)
🪛 markdownlint-cli2 (0.18.1)
example/README.md
75-75: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
95-95: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🔇 Additional comments (8)
package.json (1)
4-4: LGTM! Version bump is appropriate for new signMessage feature.The minor version increment from 2.1.3 to 2.2.0 correctly reflects the addition of new public API functionality (signMessage method) without breaking changes.
example/.env.example (1)
1-5: LGTM! Environment variables are well-structured.The environment variables provide clear configuration for the signMessage example with safe placeholder values.
example/validate-example.js (1)
1-52: LGTM! Validation script is well-structured and provides clear feedback.The script effectively validates the structure of the signMessage example using pattern matching, with appropriate exit codes and helpful user feedback.
example/package.json (1)
47-51: LGTM! Dependency changes appropriately support the signMessage example.Moving TypeScript to devDependencies and adding dotenv/ts-node provides the necessary tooling for running and testing the new signMessage example program.
lib/interfaces/index.ts (1)
110-113: LGTM! The signMessage method signature is well-defined.The interface addition correctly defines the signMessage method with appropriate type annotations for hex strings and follows the established pattern of optional chainId parameters.
CHANGELOG.md (1)
3-15: LGTM! Release notes are comprehensive and well-documented.The 2.2.0 changelog entry clearly describes the new signMessage functionality, its EIP-6492 compatibility, usage constraints, and includes helpful notes about automatic authorization creation.
__tests__/EtherspotTransactionKit.test.ts (2)
30-49: LGTM! Mock setup for viem utilities is appropriate.The extended mocks for
toHex,toRlp,zeroAddress, andsignMessagefrom viem are correctly structured to support the new signMessage test suite.
2350-2360: LGTM! Test suite provides comprehensive coverage of signMessage functionality.The test suite thoroughly covers:
- EIP-6492 signature format verification for both delegated and non-delegated states
- Error handling for incorrect wallet mode
- Authorization and signing failure scenarios
- Default chainId fallback behavior
- Support for both string and hex string messages
The test structure follows established patterns and provides good coverage of edge cases.
Also applies to: 2436-2560
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.
Actionable comments posted: 0
♻️ Duplicate comments (1)
__tests__/EtherspotTransactionKit.test.ts (1)
2447-2469: UndefinedviemSignMessageand misaligned failure-path test
(viemSignMessage as jest.Mock)is referenced butviemSignMessageis never imported or defined in this file, so this test will crash with aReferenceErrorbefore assertions run. Additionally, the "authorization creation failure" scenario is currently not guaranteed to fail for the intended reason becausegetWalletClientis not overridden here, sowalletClient.signMessagewill be undefined and can throw before the bundler authorization path is exercised.You can simplify and make the test accurate by removing the unused
viemSignMessagestub and explicitly providing a wallet client withsignMessage, so the rejection comes fromsignAuthorizationas the test name implies:@@ - const mockBundlerClient = { - signAuthorization: jest - .fn() - .mockRejectedValue(new Error('Authorization failed')), - } as any; - const mockPublicClient = { - getCode: jest.fn().mockResolvedValue('0x'), // Not installed - } as any; - - mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); - mockProvider.getBundlerClient.mockResolvedValue(mockBundlerClient); - mockProvider.getPublicClient.mockResolvedValue(mockPublicClient); - (viemSignMessage as jest.Mock).mockResolvedValue('0x' + '1'.repeat(130)); + const mockBundlerClient = { + signAuthorization: jest + .fn() + .mockRejectedValue(new Error('Authorization failed')), + } as any; + const mockPublicClient = { + getCode: jest.fn().mockResolvedValue('0x'), // Not installed + } as any; + const mockWalletClient = { + signMessage: jest.fn().mockResolvedValue('0x' + '1'.repeat(130)), + } as any; + + mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); + mockProvider.getBundlerClient.mockResolvedValue(mockBundlerClient); + mockProvider.getPublicClient.mockResolvedValue(mockPublicClient); + mockProvider.getWalletClient.mockResolvedValue(mockWalletClient); @@ - // This will fail when trying to delegate - await expect( - transactionKit.signMessage('Test message', 1) - ).rejects.toThrow(); + // This should fail when trying to create the authorization + await expect( + transactionKit.signMessage('Test message', 1) + ).rejects.toThrow('Authorization failed');This removes the undefined symbol and ensures the test actually validates the authorization failure behavior.
🧹 Nitpick comments (1)
lib/TransactionKit.ts (1)
665-852: EIP‑6492signMessageflow looks solid; a few small polish opportunitiesThe new
createEIP6492Account+signMessageflow is internally consistent with delegatedEoa mode and the tests, and the 0x6492‑wrapped signature construction using authorization RLP data looks correct for this abstraction.Minor non-blocking improvements you might consider:
- The JSDoc for
createEIP6492Accountis duplicated (Lines 665–676); you can drop one block to reduce noise.- Both the wrapper and outer
signMessagecastmessagetostring; if you need to preserve the distinction between text and raw hex messages, consider widening the parameter type and forwarding it without coercion so callers can opt into byte-level signing semantics.- To avoid redundant lookups, you could inject
walletClient(and optionallybundlerClient) intocreateEIP6492AccountfromsignMessageinstead of callinggetWalletClient(signChainId)twice.These are optional cleanups; the current implementation is functionally fine.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
__tests__/EtherspotTransactionKit.test.ts(2 hunks)lib/TransactionKit.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
lib/TransactionKit.ts (1)
lib/utils/index.ts (1)
log(70-75)
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.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (1)
lib/TransactionKit.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
lib/TransactionKit.ts (1)
lib/utils/index.ts (1)
log(70-75)
🔇 Additional comments (2)
lib/TransactionKit.ts (2)
7-14: LGTM! Imports are appropriate for EIP-6492 functionality.The new viem imports (toHex, toRlp, SignAuthorizationReturnType, LocalAccount, SignableMessage) are properly added to support the EIP-6492 message signing implementation.
808-863: I'll verify the review comment by examining the code and checking how viem'ssignMessagefunction handles message types.Based on my verification of viem's
signMessageimplementation, viem's signMessage accepts typestring | { raw: Hex | ByteArray }for the message parameter and by default signs the UTF-8 representation of the message.The critical insight is that passing a string to message (whether it starts with '0x' or not) results in viem signing the UTF-8 string representation, not raw bytes. If you need to sign raw hex data, you must use the
{ raw: '0x...' }syntax instead.In your method signature,
string | 0x${string}is a TypeScript type distinction—at runtime, both are simply strings. The type castmessage as stringon line 845 is correct and safe because0x${string}is a subtype ofstring. The suggested fix in the original review comment assumes hex strings need special handling, but viem treats them identically to plain strings unless explicitly wrapped in therawobject.Likely an incorrect or invalid review comment.
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.
Actionable comments posted: 1
🧹 Nitpick comments (4)
lib/TransactionKit.ts (2)
814-860: Consider adding message parameter validation.The method doesn't validate the
messageparameter before processing. While viem's signMessage will eventually handle it, early validation provides better error messages.Consider adding validation after line 829:
if (!message || (typeof message === 'string' && message.trim() === '')) { this.throwError( 'signMessage(): message parameter is required and must be non-empty.' ); }This ensures users get clear feedback if they pass invalid input.
711-735: Refactoring to eliminate authorization duplication has trade-offs to consider.The code does create authorization through two different paths:
- Lines 712-719: When not installed, calls
delegateSmartAccountToEoa()which internally callsbundlerClient.signAuthorization()- Lines 727-734: When already installed, calls
bundlerClient.signAuthorization()directlyWhile both paths ultimately invoke the same signing operation, the current structure provides a safety check:
delegateSmartAccountToEoa()re-checks the installation status before signing, which guards against race conditions if the state changes between the initial check at lines 704-705 and the signing operation. The simplification you propose would remove this safety mechanism.Additionally,
delegateSmartAccountToEoa()is the primary delegation API in this codebase (called 25+ times throughout), so using it for consistency may be intentional. If simplifying is desired, ensure the race condition protection is preserved or is not needed for this use case.Regarding authorization nonce increments: Each call to
signMessage()creating a fresh authorization with an incremented nonce is expected behavior. Since each authorization is used independently to wrap a single message signature and is never used in batch with other authorizations, nonce ordering is not a concern. This is standard EIP-7702 operation.__tests__/EtherspotTransactionKit.test.ts (2)
36-41: Consider making the toHex mock more robust.The fallback case
return0x${val.toString(16)};assumesvalhas atoStringmethod that accepts a radix parameter. This would throw for strings or objects without such a method.Consider handling string values explicitly:
toHex: jest.fn((val) => { if (val === undefined || val === null) return '0x0'; if (typeof val === 'bigint') return `0x${val.toString(16)}`; if (typeof val === 'number') return `0x${val.toString(16)}`; + if (typeof val === 'string') return val.startsWith('0x') ? val : `0x${val}`; return `0x${val.toString(16)}`; }),
2463-2485: Minor: Redundant mock setup in authorization failure test.Line 2479 sets up a mock for
viemSignMessage, but this test expectssignAuthorizationto fail (line 2469-2470), so the message signing step is never reached. The mock setup is harmless but unnecessary.Consider removing line 2479 for clarity:
mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); mockProvider.getBundlerClient.mockResolvedValue(mockBundlerClient); mockProvider.getPublicClient.mockResolvedValue(mockPublicClient); - (viemSignMessage as jest.Mock).mockResolvedValue('0x' + '1'.repeat(130)); // This will fail when trying to delegate
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
example/package-lock.jsonis excluded by!**/package-lock.jsonpackage-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (5)
CHANGELOG.md(1 hunks)__tests__/EtherspotTransactionKit.test.ts(2 hunks)example/README.md(0 hunks)lib/TransactionKit.ts(2 hunks)package.json(1 hunks)
💤 Files with no reviewable changes (1)
- example/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
- package.json
🧰 Additional context used
🧬 Code graph analysis (1)
lib/TransactionKit.ts (1)
lib/utils/index.ts (1)
log(70-75)
🪛 LanguageTool
CHANGELOG.md
[grammar] ~7-~7: Use a hyphen to join words.
Context: ...atures that can be validated by EIP-6492 compatible validators. The method wraps ...
(QB_NEW_EN_HYPHEN)
🔇 Additional comments (4)
CHANGELOG.md (1)
3-14: LGTM! Clear documentation of EIP-6492 signMessage support.The changelog entry accurately describes the new feature with appropriate technical details and usage constraints. The format is consistent with other entries.
Optional refinement: Static analysis suggests hyphenating "EIP-6492 compatible" on line 7, but this is a stylistic preference and not critical.
lib/TransactionKit.ts (2)
7-15: LGTM! Necessary imports for EIP-6492 implementation.All imported types and utilities are properly utilized in the signMessage implementation.
666-860: EIP-6492 format issues from previous review have been resolved.The critical issues flagged in the previous review have been fixed:
✅ Magic bytes placement (line 782): Now correctly appended at the END:
encodedWrapper + magicBytes.slice(2)✅ Encoding method (lines 768-775): Now using
encodeAbiParametersfor proper ABI encoding instead of RLP✅ Correct structure: Tuple is
(factoryAddress, factoryCalldata, originalERC1271Signature)as per EIP-6492 specThe implementation now correctly produces EIP-6492 formatted signatures with the 32-byte magic suffix.
Note: The previous review also mentioned that tests in
__tests__/EtherspotTransactionKit.test.tsand example code inexample/src/signMessage-example.tsexpected signatures to start with0x6492(which was incorrect). Please verify those have also been updated to expect the magic bytes at the end.__tests__/EtherspotTransactionKit.test.ts (1)
2368-2582: Excellent test coverage for signMessage functionality.The test suite comprehensively covers:
- ✅ EIP-6492 signature generation for both installed and non-installed EOAs
- ✅ Wallet mode validation
- ✅ Authorization and signing failure scenarios
- ✅ Default chainId handling
- ✅ Both string and hex message formats
All tests correctly verify the EIP-6492 format by checking for the magic bytes suffix (
0x6492...6492) and validating that required mocks are invoked.
| // For EIP-6492, we use the factory variant: | ||
| // abi.encode((create2Factory, factoryCalldata, originalERC1271Signature)) | ||
| // For EIP-7702, we use zero address as factory (no CREATE2 factory needed) | ||
| // and the authorization RLP as the factoryCalldata | ||
| const factoryAddress = zeroAddress; // EIP-7702 doesn't use a CREATE2 factory | ||
| const factoryCalldata = authorizationRlp; // Authorization data to activate EIP-7702 account |
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.
🧩 Analysis chain
🌐 Web query:
EIP-6492 specification format factory variant prepare variant
💡 Result:
Summary of the two ERC‑6492 wrapper variants
- Factory (deploy) variant
- When the signing contract is not yet deployed, the signature is wrapped as:
concat(abi.encode((create2Factory, factoryCalldata, originalERC1271Signature), (address, bytes, bytes)), magicBytes). - Fields:
- create2Factory: address of the factory to call
- factoryCalldata: bytes containing the factory call (deploy calldata) to be executed
- originalERC1271Signature: the ERC‑1271 signature bytes the contract would return once deployed
- magicBytes: 0x6492649264926492649264926492649264926492649264926492649264926492
- Verifier behavior: detect magicBytes, run an eth_call/multicall that executes factoryCalldata (deploy if needed), then call isValidSignature on the target address with the unwrapped originalERC1271Signature. If code exists already, still attempt isValidSignature and if it fails, optionally execute factoryCalldata and retry. [1][2]
- Prepare (make-ready) variant
- When the contract is deployed but not yet ready to validate via ERC‑1271, the signature is wrapped as:
concat(abi.encode((prepareTo, prepareData, originalERC1271Signature), (address, bytes, bytes)), magicBytes). - Fields:
- prepareTo: address (contract) to call to prepare/migrate the wallet (e.g., initialize, migrate, update)
- prepareData: calldata bytes needed to make the contract ready to verify
- originalERC1271Signature: the ERC‑1271 signature bytes
- magicBytes: same as above
- Verifier behavior: detect magicBytes, execute the prepare call (or include it in a multicall/eth_call) and then call isValidSignature with the unwrapped originalERC1271Signature. If the contract had no code, fallback to factory/deploy flow or ecrecover per spec ordering. [1][2]
Notes on verification order (required by the spec)
- Check for magicBytes first.
- If present, attempt factory/prepare calls then call isValidSignature.
- Otherwise, if code exists call isValidSignature; if that fails try factory calldata execution and retry; if no code, fall back to ecrecover as last resort. [1][2]
Sources
- ERC‑6492 specification (eips.org). [1]
- Mirror/summary (eip.info / learnblockchain). [2]
🌐 Web query:
EIP-7702 signature format EIP-6492 compatibility
💡 Result:
Short answer: they are not directly compatible.
Why (concise):
- EIP-7702 authorization entries use a fixed ECDSA tuple (y_parity, r, s) produced by an EOA signing keccak(0x05 || rlp([chain_id, address, nonce])) — i.e., a plain ECDSA signature component format expected in the authorization_list. [1][2]
- EIP-6492 defines a wrapper for ERC‑1271 contract signatures: concat(abi.encode((create2Factory, factoryCalldata, originalERC1271Signature)), magicBytes). It's intended for contract (predeploy/counterfactual) signatures and is not a simple (v,r,s) ECDSA tuple. [3][4]
Implication:
- You cannot drop an EIP‑6492 wrapper into EIP‑7702's y_parity/r/s fields and expect it to validate. EIP‑7702 expects an EOA ECDSA signature for the authorization. If you need contract/counterfactual semantics, you must instead have the EOA sign authorization data that references whatever factory/calldata/contract you need (or use other on‑chain patterns), or design a protocol layer where the EOA signs an EIP‑6492-style payload and the executor interprets it — but that is an application-level workaround, not native compatibility. [2][6][3]
Sources:
- EIP-7702 specification (authorization_list format and signing semantics). [1]
- EIP-7702 / docs summaries (authorization tuple = chain_id, address, nonce, y_parity, r, s; signer is EOA). [2][6]
- EIP-6492 specification (wrapper format and magicBytes for predeploy/ERC‑1271 signatures). [3][4]
References:
[1] EIP-7702 spec (transaction/authorization_list).
[2] eip7702.io / Alchemy / Safe docs describing signing format.
[3] EIP-6492 spec (wrapper format and magicBytes).
[4] eip.info / Fetcch integration notes for EIP-6492.
🌐 Web query:
EIP-6492 zeroAddress factory validation
💡 Result:
Short answer: the EIP-6492 spec/refs call the supplied factory via a low-level call and expect the deploy to run or revert on failure, but they do not by itself ban a zero-address factory — validators/implementations must explicitly guard against a zero factory and check the call result to avoid silent no-ops.
Details and action items:
- EIP-6492 requires that when the signature contains the ERC‑6492 suffix the verifier "call the factory first with the factoryCalldata and deploy the contract if it isn't already deployed" and reverts with a deploy-failed error if the deployment call fails. [1]
- Low-level address.call(...) does not automatically check for a contract at the target and can behave as a silent no-op (or return false) when calling an empty/EOA/non‑contract address; therefore calling address(0) without an explicit check may produce unexpected behavior if the call is not validated. Implementations must check the returned success boolean (or that create2Factory != address(0) beforehand) and revert on failure. [2][3]
Recommended validation (follow these in your verifier implementation):
- Require create2Factory != address(0) (or explicitly handle that case).
- After (create2Factory).call(factoryCalldata) assert the returned success is true; if false, revert with a deploy-failed error (as the EIP’s reference implementation does). [1]
- Optionally validate that code exists at the expected account address after the deploy call (address.code.length > 0) when appropriate.
References:
- EIP-6492 specification + reference implementation. [1]
- Notes on low-level call behavior and need to check success / EXTCODESIZE checks. [2][3]
Remove EIP-6492 encoding for EIP-7702 signatures—these standards are incompatible.
EIP-7702 authorizations require plain ECDSA signatures (y_parity, r, s) from an EOA signing keccak(0x05 || rlp([chain_id, address, nonce])), not EIP-6492 wrapper format. EIP-6492 is designed for ERC-1271 contract signatures and operates at a different protocol layer. Additionally, using zeroAddress as the factory (line 764) violates EIP-6492 expectations; validators require a non-zero factory and must explicitly validate the deploy call success. This implementation conflates two incompatible mechanisms and will fail EIP-6492 validators expecting proper factory execution.
🤖 Prompt for AI Agents
In lib/TransactionKit.ts around lines 760-765, the code is conflating EIP-6492
wrapper encoding with EIP-7702 authorizations; remove any EIP-6492 wrapping for
EIP-7702 flows and instead pass the raw ECDSA authorization data: keep
factoryAddress as zeroAddress and set factoryCalldata to the authorization RLP
(which must contain the plain ECDSA signature parts y_parity,r,s over
keccak(0x05 || rlp([chain_id,address,nonce]))); ensure the EIP-6492
encoding/CREATE2 factory call path is only used for ERC-1271/EIP-6492 cases
(with a non-zero factory and explicit deploy/validation) and gate that logic
separately so EIP-7702 never produces a wrapped factory payload.
IAmKio
left a comment
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.
Just a minor comment but other than that it's not a showstopped - LGTM
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.
Actionable comments posted: 2
♻️ Duplicate comments (1)
lib/TransactionKit.ts (1)
727-735: Refactor: Remove redundant authorization creation when already installed.When
isAlreadyInstalledis true (line 726), the EOA is already designated to the smart account. Creating a new authorization viabundlerClient.signAuthorization()(lines 731-734) is unnecessary and can cause issues:
- Nonce conflicts: Each authorization has a nonce. Creating a new one when already installed increments the nonce unnecessarily.
- Unnecessary signing: The user (or automated signer) must sign the authorization again, even though the EOA is already designated.
- Logic confusion: The authorization data is being generated for wrapping purposes, but if the EOA is already installed, no authorization data should be needed in a correctly designed EIP-6492 flow.
Note: This comment assumes the EIP-6492 wrapping is retained. However, per the previous critical issue, the entire EIP-6492 approach should be reconsidered. If you proceed with the current approach, at minimum remove this redundant authorization path.
Apply this diff to remove redundant authorization when already installed:
} else { - // When already installed, sign authorization directly - const delegateAddress = KernelVersionToAddressesMap[KERNEL_V3_3] - .accountImplementationAddress as `0x${string}`; - - authorization = await bundlerClient.signAuthorization({ - account: owner, - contractAddress: delegateAddress, - }); + // When already installed, authorization should be retrieved from the existing delegation + // or not needed for message signing. Consider if authorization is actually required here. + throw new Error( + 'EOA is already designated. Authorization data should not be recreated. Please review the EIP-6492 wrapping logic.' + ); }Or, if authorization is genuinely not needed when already installed:
- } else { - // When already installed, sign authorization directly - const delegateAddress = KernelVersionToAddressesMap[KERNEL_V3_3] - .accountImplementationAddress as `0x${string}`; - - authorization = await bundlerClient.signAuthorization({ - account: owner, - contractAddress: delegateAddress, - }); - } + } + // Note: When already installed, no new authorization is needed for message signing
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
lib/TransactionKit.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
lib/TransactionKit.ts (1)
lib/utils/index.ts (1)
log(70-75)
| /** | ||
| * Creates an EIP-6492 enabled account wrapper that intercepts signMessage calls. | ||
| * When signMessage is called on this account, it automatically wraps the signature with EIP-6492 format. | ||
| * | ||
| * @private | ||
| */ | ||
| private async createEIP6492Account( | ||
| owner: LocalAccount<string, Address>, | ||
| signChainId: number | ||
| ): Promise<LocalAccount<string, Address>> { | ||
| const bundlerClient = | ||
| await this.#etherspotProvider.getBundlerClient(signChainId); | ||
| const walletClient = | ||
| await this.#etherspotProvider.getWalletClient(signChainId); | ||
|
|
||
| // Capture 'this' context for use in the wrapper | ||
| // eslint-disable-next-line @typescript-eslint/no-this-alias | ||
| const self = this; | ||
|
|
||
| // Create a wrapper account that intercepts signMessage calls | ||
| // When walletClient.signMessage() is called with this account, viem will detect | ||
| // that the account has a signMessage method and call it directly. | ||
| // This works with ANY provider (WalletConnect, MetaMask, private keys, etc.) because: | ||
| // 1. Our wrapper's signMessage is called first | ||
| // 2. We then call walletClient.signMessage() with the original owner account | ||
| // 3. Viem delegates to the provider/transport if the owner account supports it | ||
| const eip6492Account: LocalAccount<string, Address> = { | ||
| ...owner, | ||
| async signMessage({ message }: { message: SignableMessage }) { | ||
| // First, get the standard signature from the underlying account | ||
| // This will delegate to WalletConnect/MetaMask/provider if the owner account is provider-based | ||
| const standardSignature = await walletClient.signMessage({ | ||
| account: owner, | ||
| message: message as string, | ||
| }); | ||
|
|
||
| // Get or create the authorization | ||
| const isAlreadyInstalled = | ||
| await self.isDelegateSmartAccountToEoa(signChainId); | ||
|
|
||
| let authorization: SignAuthorizationReturnType | undefined; | ||
|
|
||
| // Get authorization for EIP-6492 wrapper | ||
| // We always need authorization data for the EIP-6492 signature format, | ||
| // regardless of whether the account is already installed or not | ||
| if (!isAlreadyInstalled) { | ||
| // When not installed, use delegateSmartAccountToEoa to get authorization | ||
| // This ensures proper initialization and state management | ||
| const delegateResult = await self.delegateSmartAccountToEoa({ | ||
| chainId: signChainId, | ||
| delegateImmediately: false, | ||
| }); | ||
|
|
||
| if (!delegateResult.authorization) { | ||
| throw new Error( | ||
| 'Failed to create authorization for EIP-6492 signature' | ||
| ); | ||
| } | ||
|
|
||
| authorization = delegateResult.authorization; | ||
| } else { | ||
| // When already installed, sign authorization directly | ||
| const delegateAddress = KernelVersionToAddressesMap[KERNEL_V3_3] | ||
| .accountImplementationAddress as `0x${string}`; | ||
|
|
||
| authorization = await bundlerClient.signAuthorization({ | ||
| account: owner, | ||
| contractAddress: delegateAddress, | ||
| }); | ||
| } | ||
|
|
||
| if (!authorization) { | ||
| throw new Error( | ||
| 'Failed to create authorization for EIP-6492 signature. ' + | ||
| 'This may be due to network issues, bundler API problems, or account configuration. ' + | ||
| 'Please check your network connection and bundler API key, or try again later.' | ||
| ); | ||
| } | ||
|
|
||
| // Encode authorization for EIP-7702 | ||
| // Normalize authorization fields and provide safe fallbacks | ||
| const authChainIdHex = toHex(authorization.chainId); | ||
| const authNonceHex = toHex(authorization.nonce); | ||
| const authVHex = toHex(authorization.v ?? 0); | ||
| const authR = authorization.r; | ||
| const authS = authorization.s; | ||
| const authAddress = authorization.address; | ||
|
|
||
| // Encode authorization as RLP: [chainId, address, nonce, r, s, v] | ||
| const authorizationRlp = toRlp([ | ||
| authChainIdHex, | ||
| authAddress, | ||
| authNonceHex, | ||
| authR, | ||
| authS, | ||
| authVHex, | ||
| ]); | ||
|
|
||
| // For EIP-6492, we use the factory variant: | ||
| // abi.encode((create2Factory, factoryCalldata, originalERC1271Signature)) | ||
| // For EIP-7702, we use zero address as factory (no CREATE2 factory needed) | ||
| // and the authorization RLP as the factoryCalldata | ||
| const factoryAddress = zeroAddress; // EIP-7702 doesn't use a CREATE2 factory | ||
| const factoryCalldata = authorizationRlp; // Authorization data to activate EIP-7702 account | ||
|
|
||
| // ABI encode the tuple: (address factoryAddress, bytes factoryCalldata, bytes originalERC1271Signature) | ||
| const encodedWrapper = encodeAbiParameters( | ||
| [ | ||
| { name: 'factoryAddress', type: 'address' }, | ||
| { name: 'factoryCalldata', type: 'bytes' }, | ||
| { name: 'originalERC1271Signature', type: 'bytes' }, | ||
| ], | ||
| [factoryAddress, factoryCalldata, standardSignature] | ||
| ); | ||
|
|
||
| // EIP-6492 magic bytes: 32-byte suffix (0x6492 repeated 16 times) | ||
| const magicBytes = | ||
| '0x6492649264926492649264926492649264926492649264926492649264926492' as `0x${string}`; | ||
|
|
||
| // EIP-6492 format: encodedWrapper || magicBytes | ||
| return (encodedWrapper + magicBytes.slice(2)) as `0x${string}`; | ||
| }, | ||
| }; | ||
|
|
||
| return eip6492Account; | ||
| } |
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.
🧩 Analysis chain
🌐 Web query:
EIP-6492 signature wrapper specification counterfactual deployment
💡 Result:
Summary of EIP‑6492 (Signature wrapper for counterfactual / predeploy contracts)
-
Purpose: add a standard wrapper format so a contract-like signer that isn’t yet deployed (a counterfactual account) can produce verifiable signatures by including the factory + deploy calldata (or a prepare call) inside the signature; verifier can then deploy the account and validate via ERC‑1271. [1]
-
Wrapper format (high level): concat(abi.encode((create2Factory, factoryCalldata, originalERC1271Signature), (address, bytes, bytes)), magicBytes). magicBytes = 0x6492...6492 (32 bytes). Verifiers detect magicBytes, recover the would‑be account address from the factory + calldata (CREATE2), deploy the contract if needed, then call isValidSignature per ERC‑1271. [1][2]
-
Key rules / rationale:
- Check for magicBytes before ERC‑1271 and ecrecover to avoid collisions and to allow counterfactual signatures to remain valid after deployment.
- Recommended to use CREATE2 (predictable address) so address recovery from signature is deterministic.
- Supports two cases: not yet deployed (provide factory calldata) and deployed but not ready (provide a prepareTo + prepareData transaction to make it ready). [1][2]
-
Implementation notes:
- Reference implementations and a universal validator (on‑chain and off‑chain helpers) are provided in the EIP and community repos; verifier MUST attempt deploy when wrapper is detected. [3]
- Security considerations include replay across networks if identical factory/bytecode produces same address; usual replay protections recommended. [1][3]
Primary references
- EIP text (eips.ethereum.org). [1]
- Alternate EIP mirror / rationale (eip.info). [2]
- Reference implementation / docs (eip.directory). [3]
If you want, I can: 1) show the exact ABI encoding/bytes layout, 2) point to the GitHub reference implementation, or 3) draft verifier pseudocode. Which would you like?
🌐 Web query:
EIP-7702 EOA delegation ECDSA authorization
💡 Result:
Summary — what EIP-7702 does
- EIP-7702 adds a new transaction type that lets an EOA persistently delegate execution to a smart contract by writing a delegation indicator (0xef0100 || delegate_address) into the EOA’s code. Future code-executing operations targeting the EOA load and run the delegate contract’s runtime bytecode while keeping the EOA as the executing account. [1]
ECDSA authorizations / authorization tuple
- Delegation is granted on-chain by submitting an EIP-7702 transaction that includes one or more signed authorization tuples. Each tuple is formatted as [chain_id, authority_address, nonce, y_parity, r, s] — i.e., the chain id and ECDSA signature components used to recover the authority (signer) that authorizes the delegation. The transaction processor verifies each tuple and sets the delegation indicator using the address from the last valid tuple for an authority present in the list. [1]
Key capabilities enabled
- Batching (multiple ops in one atomic tx), sponsored transactions (payer pays gas), and privilege-delegation (sub-keys / constrained permissions via delegated contract logic). [1][2]
Important security and UX considerations
- Delegating an EOA effectively gives the delegate contract control over that account’s actions and assets; only delegate to audited/immutable contracts (prefer CREATE2 + immutable code). Hardware wallets and UX must surface and restrict arbitrary delegation (whitelists, explicit warnings). When chain_id = 0 the delegation can apply across chains — extra caution required. Wallets should prominently show the delegate contract and disallow blind signing. [1][2]
Gas & behavior notes
- EIP-7702 is a new EIP-2718 tx type with additional intrinsic gas for each authorization; senders pay for all tuples. Some opcodes (CODESIZE/CODECOPY) behave differently (they operate on the delegation indicator vs. the underlying code), and clients must handle delegation-related transaction propagation risks (e.g., pending tx analysis). Loops of delegation are not followed beyond the first resolution. [1]
Further reading
- EIP text (spec + spec examples and gas rules). [1]
- Ethereum.org / security & hardware-wallet guidance (best practices, phishing risks). [2]
- Third‑party technical overviews (implementation notes). [3]
Sources
[1] EIP-7702 specification — eips.ethereum.org/eip-7702.
[2] ethereum.org guidance / Pectra 7702 page (security & HW wallet guidelines).
[3] Biconomy / ecosystem docs (technical overview).
🌐 Web query:
EIP-6492 EIP-7702 compatibility combined usage
💡 Result:
Short answer
- EIP-6492 and EIP-7702 are complementary and can be used together: 6492 is a signature/wrapper standard for validating signatures for counterfactual (not-yet-deployed) contract wallets (ERC‑1271 extension), while 7702 is a protocol-level “set EOA code / authorization” mechanism that lets an EOA temporarily delegate execution to contract code for a transaction. Use 7702 to let an EOA run smart-account logic (or point to an ERC‑4337 wallet) and use 6492 to validate signatures produced by counterfactual contract wallets that may be deployed by factories. [1][2][3]
Key interoperability points (practical)
- Separation of concerns: 7702 operates at the transaction/authorization layer (who authorizes code to run), 6492 operates at signature verification (how to prove a contract-wallet signature before deployment). They don’t conflict. [2][1]
- Typical combined flow:
- An EOA signs a 7702 authorization delegating to a smart-wallet implementation (or an ERC‑4337-compatible account). The transaction includes that authorization. [2][3]
- The delegated smart-wallet (or an account factory used by it) may produce/require ERC‑1271-style signatures for user ops; if those signatures were made counterfactually (before contract code exists) they should be wrapped per EIP‑6492 so verifiers can deploy or simulate the factory and call isValidSignature. [1][2]
- Relayers / bundlers / verifiers must implement both behaviors:
- Accept and validate 7702 authorization tuples and include them appropriately in the tx pipeline. [2][3]
- Detect the ERC‑6492 magic-suffix on signatures, perform the factory deployment/simulate multicall as specified, then call isValidSignature (or fallback to ecrecover) in the order mandated by EIP‑6492. [1]
- Security and UX notes:
- Watch nonce/authorization lifetimes: 7702 authorizations are bound to chain/nonce semantics; 6492 signatures can be created before deployment and could be replayed if factories/bytecode are reproducible across chains—apply replay protections. [1][2]
- If a verifier skips the 6492 deploy step when code already exists the EIP requires trying both deployed‑contract verification and deploy‑then‑verify to handle “deployed but not ready” cases. [1]
References
- EIP‑6492 (ERC‑6492) specification and reference implementations. [1]
- EIP‑7702 overview / specification (Set EOA code / authorizations). [2]
- Implementation / developer guides (Alchemy / ecosystem docs) illustrating how 7702 is used with smart-account flows and bundlers. [3]
Clarify the intended signature flow: EIP-6492 wraps ERC-1271 contract signatures, not EOA ECDSA signatures.
EIP-6492 and EIP-7702 are complementary standards that operate at different layers—they are not architecturally incompatible. EIP-7702 handles transaction-level authorization (delegating an EOA to a smart contract), while EIP-6492 handles signature verification for counterfactual contract wallets. However, this implementation has critical flaws in how it applies EIP-6492:
The signature being wrapped is incorrect for EIP-6492:
-
Lines 703–735: The code wraps the output of
walletClient.signMessage()(an EOA ECDSA signature) in EIP-6492 format. EIP-6492 is designed to wrap ERC-1271 contract signatures produced by a smart contract wallet, not raw ECDSA signatures from an EOA. A verifier will:- Detect the magic suffix
- Attempt to deploy the contract using
zeroAddress(line 768) → fails or no-op - Call
isValidSignature()on an EOA → fails (EOAs don't implement ERC-1271) - Fall back to
ecrecoveron the underlying ECDSA → may succeed, but defeats the purpose of EIP-6492 wrapping
-
Line 768: zeroAddress as factory — EIP-6492 validators expect a non-zero factory address to call for deployment. Using
zeroAddressviolates the spec and will cause silent failures or no-ops in low-level contract calls. -
Lines 754–762: Authorization RLP as factoryCalldata — The authorization RLP encoding is not factory deployment calldata. Validators will attempt to execute this as a function call, which will fail.
-
Lines 727–735: Redundant authorization when already installed — When
isAlreadyInstalledis true, creating a new authorization viabundlerClient.signAuthorization()is unnecessary and creates nonce conflicts.
Correct approach:
If you want to use EIP-6492 with EIP-7702, the intended flow is:
- The delegated smart contract produces an ERC-1271 signature (via
isValidSignature()) - Wrap that contract signature in EIP-6492 format if the contract is counterfactual (not yet deployed)
- Verifiers will deploy the contract via the factory and validate the signature
For simple EOA message signing with EIP-7702 delegation, you should return the plain ECDSA signature without EIP-6492 wrapping.
| /** | ||
| * Signs a message using EIP-6492 format for EIP-7702 wallets. | ||
| * This creates a signature that can be validated before the smart account is deployed/activated. | ||
| * | ||
| * @param message - The message to sign (string or hex string). | ||
| * @param chainId - (Optional) The chain ID to use. If not provided, uses the provider's current chain ID. | ||
| * @returns A promise that resolves to the EIP-6492 formatted signature as a hex string. | ||
| * @throws {Error} If called in 'modular' wallet mode (only available in 'delegatedEoa' mode). | ||
| * @throws {Error} If signing fails or authorization cannot be created. | ||
| * | ||
| * @remarks | ||
| * - Only available in 'delegatedEoa' wallet mode. | ||
| * - Creates an EIP-6492 compatible signature that wraps the standard signature with deployment data. | ||
| * - The signature format follows EIP-6492: `abi.encode((factoryAddress, factoryCalldata, originalSignature)) || magicBytes` | ||
| * where magicBytes is the 32-byte suffix `0x6492...` (repeated 16 times) | ||
| * - If the EOA is not yet designated, this will create the authorization automatically. | ||
| * - The signature can be validated by contracts that support EIP-6492, even before the smart account is activated. | ||
| * - Uses WalletClient.signMessage() which delegates to the underlying provider/transport if the account supports it. | ||
| * This allows signing with provider-based accounts (e.g., WalletConnect, MetaMask, hardware wallets) when using | ||
| * viemLocalAccount that wraps the provider, not just direct private key accounts. | ||
| * - The implementation creates an EIP-6492 enabled account wrapper that intercepts signMessage calls, | ||
| * allowing walletClient.signMessage() to directly return EIP-6492 formatted signatures. | ||
| * - Works with WalletConnect and other providers: The wrapper's signMessage is called first, then it delegates | ||
| * to the provider for actual signing, ensuring the signature is wrapped in EIP-6492 format regardless of provider. | ||
| */ | ||
| async signMessage( | ||
| message: string | `0x${string}`, | ||
| chainId?: number | ||
| ): Promise<`0x${string}`> { | ||
| const walletMode = this.#etherspotProvider.getWalletMode(); | ||
| const signChainId = chainId || this.#etherspotProvider.getChainId(); | ||
|
|
||
| log('signMessage(): Called', { message, signChainId }, this.debugMode); | ||
|
|
||
| if (walletMode !== 'delegatedEoa') { | ||
| this.throwError( | ||
| "signMessage() is only available in 'delegatedEoa' wallet mode. " + | ||
| `Current mode: '${walletMode}'. ` + | ||
| 'This method creates EIP-6492 compatible signatures for EIP-7702 wallets.' | ||
| ); | ||
| } | ||
|
|
||
| try { | ||
| // Get the owner account (EOA) | ||
| const owner = await this.#etherspotProvider.getOwnerAccount(signChainId); | ||
|
|
||
| // Create EIP-6492 enabled account wrapper | ||
| const eip6492Account = await this.createEIP6492Account( | ||
| owner, | ||
| signChainId | ||
| ); | ||
|
|
||
| // Call the wrapper's signMessage directly so the EIP-6492 logic always runs, | ||
| // regardless of wallet client behavior or mocks. | ||
| const signature = await eip6492Account.signMessage({ | ||
| message: message as SignableMessage, | ||
| }); | ||
|
|
||
| log( | ||
| 'signMessage(): EIP-6492 signature created', | ||
| { | ||
| signatureLength: signature.length, | ||
| }, | ||
| this.debugMode | ||
| ); | ||
|
|
||
| return signature; | ||
| } catch (error) { | ||
| log('signMessage(): Failed', error, this.debugMode); | ||
| throw error; | ||
| } | ||
| } |
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.
Critical: signMessage documentation claims are inaccurate given the implementation issues.
The method documentation makes several claims that are not supported by the current implementation:
-
Line 809: "The signature can be validated by contracts that support EIP-6492" — This is inaccurate. The signature format used here is incompatible with EIP-6492 validators due to the architectural issues detailed in the review of
createEIP6492Account(lines 666-791). -
Lines 807-808: "magicBytes is the 32-byte suffix
0x6492..." — While this is now correct (fixed from past review), the overall format remains invalid for EIP-6492 validation. -
Line 808: "If the EOA is not yet designated, this will create the authorization automatically" — This is misleading. The authorization is created, but embedding it in EIP-6492 format doesn't enable pre-deployment validation as EIP-6492 is intended for contract signatures, not EOA signatures.
Architectural concern:
The method's core premise—wrapping EIP-7702 EOA signatures in EIP-6492 format—is fundamentally flawed. This creates signatures that:
- Won't validate correctly with EIP-6492 validators
- Confuse two different signature standards (EOA ECDSA vs. contract ERC-1271)
- Provide no actual benefit over plain EOA signatures
Recommendation:
If the goal is to enable message signing for EOAs that may be delegated to smart accounts:
- For non-delegated EOAs: Return the plain ECDSA signature from
walletClient.signMessage() - For delegated EOAs: After delegation, call
isValidSignature()on the delegated smart account contract (if it implements ERC-1271) - Remove EIP-6492 wrapping entirely from this flow
Update the method to:
async signMessage(
message: string | `0x${string}`,
chainId?: number
): Promise<`0x${string}`> {
const walletMode = this.#etherspotProvider.getWalletMode();
const signChainId = chainId || this.#etherspotProvider.getChainId();
if (walletMode !== 'delegatedEoa') {
this.throwError(
"signMessage() is only available in 'delegatedEoa' wallet mode. " +
`Current mode: '${walletMode}'.`
);
}
try {
const owner = await this.#etherspotProvider.getOwnerAccount(signChainId);
const walletClient = await this.#etherspotProvider.getWalletClient(signChainId);
// Simply sign with the EOA - no EIP-6492 wrapping
const signature = await walletClient.signMessage({
account: owner,
message: message as string,
});
log('signMessage(): EOA signature created', { signatureLength: signature.length }, this.debugMode);
return signature;
} catch (error) {
log('signMessage(): Failed', error, this.debugMode);
throw error;
}
}Then, if needed, provide a separate method to check delegation status and call isValidSignature() on the delegated contract.
🤖 Prompt for AI Agents
lib/TransactionKit.ts lines 793-864: The current signMessage implementation and
JSDoc incorrectly claim it produces EIP-6492-valid signatures by wrapping EOA
signatures, which is architecturally invalid; update the implementation to stop
wrapping messages in EIP-6492 and instead call the
provider/walletClient.signMessage to return the plain ECDSA signature (using
getWalletClient and passing the owner account), update the thrown error/message
to only reference 'delegatedEoa' availability, and change the JSDoc to remove
claims about EIP-6492 validation, magic bytes, and pre-deployment validation
while adding a note that delegation validation should be done by calling
isValidSignature on the delegated smart account (provide that as a separate
helper if needed).
Description
Added
signMessagefunction that creates EIP-6492 compatible signatures for EIP-7702 wallets, allowing message signing before and after smart account delegation.Changes
signMessage()method toEtherspotTransactionKitclassIInitialinterface to includesignMessagesignatureHow Has This Been Tested?
Unit testing
Screenshots (if appropriate):
Example Output
Types of changes
Summary by CodeRabbit
New Features
Tests
Changelog
✏️ Tip: You can customize this high-level summary in your review settings.