Skip to content

Conversation

@ch4r10t33r
Copy link
Member

@ch4r10t33r ch4r10t33r commented Nov 20, 2025

Description

Added signMessage function that creates EIP-6492 compatible signatures for EIP-7702 wallets, allowing message signing before and after smart account delegation.

Changes

  • ✅ Added signMessage() method to EtherspotTransactionKit class
  • ✅ Updated IInitial interface to include signMessage signature
  • ✅ Added comprehensive unit tests (7 test cases)
  • ✅ Created example program demonstrating usage

How Has This Been Tested?

Unit testing

npm test -- --testNamePattern="signMessage"

Screenshots (if appropriate):

Example Output

============================================================
Example 1: Sign Message with Non-Delegated EOA
============================================================

EOA Address: 0x1234...
Is Delegated: No
Message to Sign: Hello, World! This is a test message.
EIP-6492 Format Valid: ✓ Yes
Magic Prefix: 0x6492
Signature: 0x6492abc123...
Total Signature Length: 400+ chars

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Summary by CodeRabbit

  • New Features

    • Added signMessage() for EIP-6492-compatible signatures on EIP-7702 delegated EOA wallets
    • Automatically creates EIP-7702 authorization if EOA is not yet deployed
    • Supports text and hex messages; signatures compatible with EIP-6492 validators
    • Available only in delegated EOA wallet mode
  • Tests

    • Expanded tests covering signMessage flows, edge cases, and error paths
  • Changelog

    • Bumped release to 2.1.4 and added entry detailing the above features

✏️ Tip: You can customize this high-level summary in your review settings.

@ch4r10t33r ch4r10t33r requested review from IAmKio and RanaBug and removed request for IAmKio November 20, 2025 11:01
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 20, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary
Version & Changelog
CHANGELOG.md, package.json
Bumps package to 2.1.4 and adds CHANGELOG entry documenting EIP-6492 signMessage support, delegatedEoa restriction, and auto-authorization behavior.
Core Implementation
lib/TransactionKit.ts
Adds `signMessage(message: string
Interface Definition
lib/interfaces/index.ts
Adds signMessage(message: string | \0x${string}`, chainId?: number): Promise<`0x${string}`>;toIInitial`.
Tests
__tests__/EtherspotTransactionKit.test.ts
Adds extensive tests and mocks for signMessage flows under delegatedEoa: authorization creation/retrieval, message formats (hex/plain), chainId defaulting, error paths, and integrations with signer/bundler/public clients.
Examples & Docs
example/.env.example, example/README.md
Adds example env vars (demo wallet PK, bundler URL, API key, chain id) and minor README whitespace cleanup.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Pay attention to: EIP-6492 byte layout/concatenation, correct use of viem encoding helpers, delegatedEoa validation and automatic authorization creation, and exhaustive test expectations for error conditions.

Possibly related PRs

Suggested reviewers

  • IAmKio

Poem

🐰 I nibble bytes and wrap a sig so spry,
0x6492 hops into the sky,
Owner found, authorization spun,
A signature blooms when the work is done,
Little rabbit claps — hop, deploy, goodbye! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title describes adding EIP-6492 compatible signMessage, which is the main feature added, but uses 'fix:' prefix when the PR implements a new feature, not a bug fix. Consider using 'feat:' prefix instead of 'fix:' to accurately reflect that this is a new feature implementation, not a bug fix.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description includes a summary, changes list, testing instructions, example output, and properly marked the new feature checkbox, covering all required template sections adequately.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch signature-fix

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

Yooo! You forgot to bump the version in package.json!

@ch4r10t33r ch4r10t33r requested a review from IAmKio November 20, 2025 11:03
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 and SignAuthorizationReturnType usage in signMessage.

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:

  1. authorization.data likely not present on viem’s SignAuthorizationReturnType.
    The type exported from viem/accounts currently exposes fields like chainId, contractAddress (or address in some stacks), nonce, r, s, yParity, but not a data field. As written:

    const authorizationData = authorization.data || '0x';

    will resolve to '0x' for viem’s SignedAuthorization, so your deploymentData will 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 from authorization instead of relying on .data.

  2. 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 suffix 0x6492…6492 for detection; most generic validators (e.g. Solady’s isValidERC6492SignatureNow*, Coinbase’s PublicERC6492Validator) 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.

  3. Message typing for hex inputs.
    walletClient.signMessage is 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 for 0x messages you may want to branch on typeof message === 'string' && message.startsWith('0x') and pass message: { 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_KEY is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9274f02 and 106f7dc.

⛔ Files ignored due to path filters (1)
  • example/package-lock.json is 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, and signMessage from 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

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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: Undefined viemSignMessage and misaligned failure-path test

(viemSignMessage as jest.Mock) is referenced but viemSignMessage is never imported or defined in this file, so this test will crash with a ReferenceError before assertions run. Additionally, the "authorization creation failure" scenario is currently not guaranteed to fail for the intended reason because getWalletClient is not overridden here, so walletClient.signMessage will be undefined and can throw before the bundler authorization path is exercised.

You can simplify and make the test accurate by removing the unused viemSignMessage stub and explicitly providing a wallet client with signMessage, so the rejection comes from signAuthorization as 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‑6492 signMessage flow looks solid; a few small polish opportunities

The new createEIP6492Account + signMessage flow 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 createEIP6492Account is duplicated (Lines 665–676); you can drop one block to reduce noise.
  • Both the wrapper and outer signMessage cast message to string; 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 optionally bundlerClient) into createEIP6492Account from signMessage instead of calling getWalletClient(signChainId) twice.

These are optional cleanups; the current implementation is functionally fine.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 106f7dc and 6d436f8.

📒 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)

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6d436f8 and 1fb983c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is 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's signMessage function handles message types.

Based on my verification of viem's signMessage implementation, viem's signMessage accepts type string | { 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 cast message as string on line 845 is correct and safe because 0x${string} is a subtype of string. 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 the raw object.

Likely an incorrect or invalid review comment.

RanaBug
RanaBug previously approved these changes Dec 9, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 message parameter 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 calls bundlerClient.signAuthorization()
  • Lines 727-734: When already installed, calls bundlerClient.signAuthorization() directly

While 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 return 0x${val.toString(16)}; assumes val has a toString method 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 expects signAuthorization to 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1fb983c and 5347b3c.

⛔ Files ignored due to path filters (2)
  • example/package-lock.json is excluded by !**/package-lock.json
  • package-lock.json is 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 encodeAbiParameters for proper ABI encoding instead of RLP

Correct structure: Tuple is (factoryAddress, factoryCalldata, originalERC1271Signature) as per EIP-6492 spec

The 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.ts and example code in example/src/signMessage-example.ts expected signatures to start with 0x6492 (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.

Comment on lines +760 to +765
// 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
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

EIP-6492 specification format factory variant prepare variant

💡 Result:

Summary of the two ERC‑6492 wrapper variants

  1. 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]
  1. 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
IAmKio previously approved these changes Dec 9, 2025
Copy link
Contributor

@IAmKio IAmKio left a 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

@RanaBug RanaBug dismissed stale reviews from IAmKio and themself via 0677db9 December 9, 2025 17:49
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 isAlreadyInstalled is true (line 726), the EOA is already designated to the smart account. Creating a new authorization via bundlerClient.signAuthorization() (lines 731-734) is unnecessary and can cause issues:

  1. Nonce conflicts: Each authorization has a nonce. Creating a new one when already installed increments the nonce unnecessarily.
  2. Unnecessary signing: The user (or automated signer) must sign the authorization again, even though the EOA is already designated.
  3. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 5347b3c and 0677db9.

📒 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)

Comment on lines +666 to +791
/**
* 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;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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:
    1. 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]
    2. 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:

  1. 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 ecrecover on the underlying ECDSA → may succeed, but defeats the purpose of EIP-6492 wrapping
  2. Line 768: zeroAddress as factory — EIP-6492 validators expect a non-zero factory address to call for deployment. Using zeroAddress violates the spec and will cause silent failures or no-ops in low-level contract calls.

  3. 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.

  4. Lines 727–735: Redundant authorization when already installed — When isAlreadyInstalled is true, creating a new authorization via bundlerClient.signAuthorization() is unnecessary and creates nonce conflicts.

Correct approach:

If you want to use EIP-6492 with EIP-7702, the intended flow is:

  1. The delegated smart contract produces an ERC-1271 signature (via isValidSignature())
  2. Wrap that contract signature in EIP-6492 format if the contract is counterfactual (not yet deployed)
  3. 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.

Comment on lines +793 to +864
/**
* 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;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: signMessage documentation claims are inaccurate given the implementation issues.

The method documentation makes several claims that are not supported by the current implementation:

  1. 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).

  2. 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.

  3. 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:

  1. For non-delegated EOAs: Return the plain ECDSA signature from walletClient.signMessage()
  2. For delegated EOAs: After delegation, call isValidSignature() on the delegated smart account contract (if it implements ERC-1271)
  3. 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).

@RanaBug RanaBug self-requested a review December 9, 2025 17:58
@RanaBug RanaBug merged commit d00b679 into master Dec 9, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants