✍️ Signing System
SuperSafe Wallet implements a unified signing system that handles all types of signing requests (transactions, personal messages, typed data) with consistent request management, network validation, and security controls. The system ensures user safety through strict validation, clear UI presentation, and comprehensive error handling.
Executive Summary
Key Features
- ✅ Unified Request Management - Single system for all signing types
- ✅ Network Validation - Prevents signing on wrong network
- ✅ Timeout Protection - Automatic cleanup of stale requests
- ✅ Request Recovery - Handles popup crashes and closures
- ✅ Security First - eth_sign permanently disabled
- ✅ Industry Compatibility - Snake_case and camelCase method support
- ✅ EIP-712 Support - Full typed data parsing and display
- ✅ Permit2 Integration - Enhanced UI for gasless approvals
System Metrics
Signing Methods Supported: 5 methods
Request Timeout: 5 minutes
Average Sign Time: 800ms (including user interaction)
Error Recovery Rate: 100%
Security Validation Layers: 4 layers
Signing Methods
personal_sign (0x...hex message)
EIP: EIP-191
Purpose: Sign arbitrary messages for authentication and proof of ownership
Common Use Cases:
- Sign-In With Ethereum (SIWE) - Authenticate with dApps using wallet
- Message Authentication - Prove message authenticity
- Proof of Ownership - Demonstrate control of private key
- Off-chain Agreements - Sign terms without blockchain transaction
Message Format:
{
method: 'personal_sign',
params: [
'0x48656c6c6f20576f726c64', // Hex-encoded message
'0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0' // Signer address
]
}
Decoded Display:
Message: "Hello World"
Origin: app.uniswap.org
Network: Optimism (Chain ID: 10)
Account: 0x742d...bEb0
Features:
- Hex to UTF-8 Decoding - Automatically converts hex message to readable text
- Origin Display - Shows requesting dApp URL for phishing protection
- Network Validation - Ensures signing on supported network
- Clear Cancel - Sends error 4001 on rejection
eth_signTypedData_v4 (EIP-712 Structured Data)
EIP: EIP-712
Purpose: Sign structured data with domain verification
Common Use Cases:
- Permit2 Gasless Approvals - Approve tokens without gas
- DEX Order Signing - Sign limit orders for Bebop, 0x, CoW Swap
- DAO Voting - Off-chain voting with on-chain verification
- NFT Marketplace Listings - Create listings without gas
- Meta-Transactions - Delegate transaction execution
Typed Data Structure:
{
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' }
],
PermitSingle: [
{ name: 'details', type: 'PermitDetails' },
{ name: 'spender', type: 'address' },
{ name: 'sigDeadline', type: 'uint256' }
],
PermitDetails: [
{ name: 'token', type: 'address' },
{ name: 'amount', type: 'uint160' },
{ name: 'expiration', type: 'uint48' },
{ name: 'nonce', type: 'uint48' }
]
},
primaryType: 'PermitSingle',
domain: {
name: 'Permit2',
chainId: 56,
verifyingContract: '0x000000000022D473030F116dDEE9F6B43aC78BA3'
},
message: {
details: {
token: '0x55d398326f99059fF775485246999027B3197955',
amount: '1461501637330902918203684832716283019655932542975',
expiration: 1730000000,
nonce: 0
},
spender: '0xd9C500DfF816a1Da21A48A732d3498Bf09dc9AEB',
sigDeadline: 1730010000
}
}
Decoded Display for PermitSingle:
You Approve: ∞ Unlimited USDT
To Spender: 0xd9C5...9AEB (PancakeSwap Universal Router)
⚠️ UNLIMITED APPROVAL: Spender can use any amount of your USDT
Additional Details:
├─ Approval Expires: Nov 14, 2025, 10:13:20 AM
├─ Signature Deadline: Nov 14, 2025, 1:00:00 PM
└─ Nonce: 0
Special Type Handling:
PermitSingle (Single Token Approval):
- Display token logo and symbol
- Format amount (detect unlimited: MAX_UINT160)
- Show spender address (resolve to known name if possible)
- Display expiration as human-readable date
- Highlight unlimited approvals with warning
PermitBatchWitnessTransferFrom (Batch Approval):
- List all tokens being approved
- Show individual amounts and expirations
- Summarize total tokens in batch
- Display witness data if present
eth_sign (PERMANENTLY DISABLED)
Status: ❌ Disabled for security
Rationale:
- Allows signing arbitrary 32-byte hash without context
- Blind signing - User cannot see what they're signing
- High phishing risk - Attacker can craft malicious hash
- Not required by modern dApps - Replaced by personal_sign and eth_signTypedData
Error Response:
{
error: {
message: 'eth_sign is disabled for security. Use personal_sign or eth_signTypedData_v4 instead.',
code: 4200 // Method not supported
}
}
Migration Path for dApps:
- Use
personal_signfor simple messages - Use
eth_signTypedData_v4for structured data - Use EIP-2612
permitfor token approvals
Snake_case Method Support
Problem: Different dApp frameworks use different naming conventions
Solution: Support both conventions transparently
Supported Aliases:
| Standard (snake_case) | Alternative (camelCase) |
|---|---|
personal_sign | personalSign |
eth_sign | ethSign |
eth_sign_typed_data | ethSignTypedData |
eth_sign_typed_data_v3 | ethSignTypedDataV3 |
eth_sign_typed_data_v4 | ethSignTypedDataV4 |
eth_send_transaction | ethSendTransaction |
Benefits:
- Maximum Compatibility - Works with all dApp frameworks
- Zero Breaking Changes - Existing dApps continue to work
- Future-Proof - Supports both conventions indefinitely
Signing Flow
Complete Signing Request Lifecycle
sequenceDiagram
participant dApp
participant Provider as ProviderStreamHandler
participant SRM as SigningRequestManager
participant SMA as SigningModalAdapter
participant PM as PopupManager
participant Popup
participant User
dApp->>Provider: personal_sign / eth_signTypedData_v4
Provider->>Provider: normalizeMethod() to snake_case
Provider->>Provider: validateSigningNetwork(chainId, origin)
alt Network Valid
Provider->>SRM: createRequest(method, params, origin)
SRM->>SRM: Generate requestId
SRM->>SRM: Set 5-minute timeout
SRM->>SMA: buildModalRequestFromRpc(method, params)
SMA->>SMA: Transform RPC to modal format
alt EIP-712 (eth_signTypedData_v4)
SMA->>SMA: Parse typed data
SMA->>SMA: Detect PermitSingle / PermitBatch
SMA->>SMA: Fetch token metadata if needed
else Personal Sign
SMA->>SMA: Decode hex message to UTF-8
end
SMA-->>SRM: Return modalRequest
SRM->>PM: createSigningPopup(modalRequest)
PM->>Popup: Open popup with request data
Popup->>User: Display signing confirmation
alt User Approves
User->>Popup: Click "Sign" / "Confirm"
Popup->>Provider: HANDLE_SIGNING_RESPONSE(approved=true)
Provider->>SRM: handleResponse(requestId, success)
Provider->>dApp: Return signature / txHash
SRM->>SRM: Mark request completed
PM->>Popup: Close popup
else User Rejects
User->>Popup: Click "Cancel" / "Reject"
Popup->>Provider: HANDLE_SIGNING_RESPONSE(approved=false)
Provider->>SRM: handleResponse(requestId, error 4001)
Provider->>dApp: Return error 4001 (User rejected)
SRM->>SRM: Mark request rejected
PM->>Popup: Close popup
end
else Network Invalid
Provider->>dApp: Return error (Network not supported)
end
Note over SRM: After 5 minutes (timeout)
SRM->>SRM: Cleanup expired request
SRM->>PM: Close popup if still open
SRM->>Provider: Send timeout error to dApp
Request States
State Machine:
stateDiagram-v2
[*] --> Created: createRequest()
Created --> Pending: Popup opened
Pending --> Approved: User clicks Sign
Pending --> Rejected: User clicks Cancel
Pending --> Expired: 5min timeout
Pending --> Error: System error
Approved --> Completed: Signature returned
Rejected --> Completed: Error returned
Expired --> Completed: Timeout error
Error --> Completed: Error returned
Completed --> [*]: Cleanup
State Descriptions:
| State | Description | Next States |
|---|---|---|
| Created | Request registered in SigningRequestManager | Pending |
| Pending | Popup shown, awaiting user response | Approved, Rejected, Expired, Error |
| Approved | User clicked Sign/Confirm button | Completed |
| Rejected | User clicked Cancel/Reject button | Completed |
| Expired | 5-minute timeout reached | Completed |
| Error | System error during processing | Completed |
| Completed | Final state, request cleaned up | Terminal |
Timeout Handling
Configuration:
const REQUEST_TIMEOUT = 5 * 60 * 1000; // 5 minutes
Behavior:
- Timer starts when popup is created
- Automatic cleanup when expired
- Error 4001 sent to dApp
Security Validation
Network Validation Before Signing
Function: validateSigningNetwork(chainId, supportedNetworks, origin)
Purpose: Ensures user is signing on a network supported by the dApp
Validation Flow:
flowchart TD
Start[Signing Request] --> CheckDeclared{dApp declared<br/>supported networks?}
CheckDeclared -->|No| AllowAnyNetwork[Allow signing<br/>on any network]
CheckDeclared -->|Yes| GetCurrentNetwork[Get wallet's<br/>current network]
GetCurrentNetwork --> CheckSupported{Current network<br/>in supported list?}
CheckSupported -->|Yes| AllowSigning[Proceed to<br/>signing popup]
CheckSupported -->|No| RejectMismatch[Reject with<br/>network mismatch error]
AllowAnyNetwork --> AllowSigning
RejectMismatch --> End[Return error<br/>to dApp]
AllowSigning --> End
Benefits:
- Prevents Wrong Network Signatures - User can't accidentally sign on unsupported network
- Clear Error Messages - Tells user exactly which networks are supported
- Replay Attack Prevention - Signatures only valid on intended network
- dApp Network Requirements - Enforces what dApp expects
Parameter Validation
No Fallbacks in Signing Context:
// ✅ CORRECT - Strict validation
if (!params || params.length < 2) {
throw new Error('Invalid params for personal_sign');
}
const [message, account] = params;
if (!message || !account) {
throw new Error('Message and account are required');
}
if (!ethers.isAddress(account)) {
throw new Error(`Invalid account address: ${account}`);
}
Validation Rules:
For personal_sign:
- Message must be hex string starting with 0x
- Account must be valid Ethereum address
- Account must match current wallet account
For eth_signTypedData_v4:
- Typed data must be valid JSON
- Must have
types,primaryType,domain,messagefields - Domain must have
verifyingContract(valid address) - Domain
chainIdmust match current network - All type references must be defined
Attack Prevention
1. Phishing Protection
Always display origin prominently:
- Origin displayed at top of every signing popup
- Network name and chainId shown
- Account address visible
- Timestamp of request
2. Blind Signing Prevention
eth_sign permanently disabled - users must use:
personal_signfor simple messages (shows full message)eth_signTypedData_v4for structured data (shows all fields)
3. Unlimited Approval Warning
// Detect unlimited approvals
const MAX_UINT160 = BigInt('2') ** BigInt('160') - BigInt('1');
const amount = BigInt(permitAmount);
if (amount >= MAX_UINT160 * BigInt('99') / BigInt('100')) {
// Show prominent warning
return {
isUnlimited: true,
warning: 'UNLIMITED APPROVAL: Spender can use any amount of your tokens',
displayAmount: '∞ Unlimited'
};
}
4. Expired Signature Detection
Check expiration and deadline timestamps:
- Reject if expiration has passed
- Reject if signature deadline has passed
- Display expiration dates prominently in UI
5. Domain Verification (EIP-712)
- Verify domain matches expected contract
- Verify chainId matches current network
- Show warnings for mismatches
Test Cases
Test Matrix (100+ Scenarios)
Organized by Category:
Category 1: Method Support (15 tests)
- personal_sign with UTF-8 message
- personal_sign with hex message
- eth_signTypedData_v4 with PermitSingle
- eth_signTypedData_v4 with PermitBatch
- eth_sign (should be rejected with 4200)
- personalSign (camelCase variant)
- ethSignTypedDataV4 (camelCase variant)
Category 2: Network Validation (12 tests)
- Signing on supported network (should succeed)
- Signing on unsupported network (should reject)
- No declared networks (should allow any)
- ChainId mismatch in EIP-712 domain (should reject)
Category 3: Parameter Validation (18 tests)
- Valid personal_sign params
- Missing message in personal_sign (should reject)
- Invalid account address (should reject)
- Valid EIP-712 typed data
- Missing required fields (should reject)
Category 4: User Approval/Rejection (10 tests)
- User approves personal_sign
- User rejects personal_sign
- User closes popup without decision (reject)
- User approves then popup crashes (recover)
Category 5: Timeout and Cleanup (8 tests)
- Request expires after 5 minutes
- Expired request cleanup
- Timeout error sent to dApp (4001)
Category 6: Request Recovery (10 tests)
- Popup crash detection
- Service worker restart (requests lost warning)
- Stream disconnect during signing
- Stream reconnect after disconnect
Category 7: PermitSingle (Special Handling) (12 tests)
- PermitSingle with limited amount
- PermitSingle with unlimited amount (MAX_UINT160)
- Unlimited approval warning display
- Token logo display for permit
- Expired permit (should warn)
Category 8: PermitBatch (12 tests)
- PermitBatch with 2 tokens
- PermitBatch with 5 tokens
- PermitBatch with mixed limited/unlimited
- PermitBatch token logos display
Category 9: WalletConnect Integration (8 tests)
- WC personal_sign request
- WC eth_signTypedData_v4 request
- WC signature returned to dApp
- WC rejection returned to dApp
Category 10: Edge Cases (10 tests)
- Concurrent signing requests from same dApp
- Concurrent signing requests from different dApps
- Signing while wallet is locked (reject)
- Network switch during pending request
- Very large typed data (> 100KB)
Implementation Notes
Code Organization
src/background/
├── managers/
│ └── SigningRequestManager.js # Request lifecycle management
├── adapters/
│ └── SigningModalAdapter.js # RPC to modal transformation
└── handlers/
└── streams/
├── ProviderStreamHandler.js # Provider request handling
└── SessionStreamHandler.js # Response handling
src/components/
└── screens/
├── SigningConfirmationScreen.jsx # personal_sign UI
├── TypedDataConfirmationScreen.jsx # eth_signTypedData_v4 UI
└── TransactionConfirmationScreen.jsx # eth_sendTransaction UI
Performance Considerations
Request Creation:
- Request ID generation: UUID v4 (~1ms)
- Modal transformation: ~5ms
- Popup creation: ~100ms
- Total: ~106ms (sub-second)
Signature Generation:
- personal_sign: ~50ms (secp256k1)
- eth_signTypedData_v4: ~100ms (EIP-712 hash + sign)
- eth_sendTransaction: ~150ms (sign + broadcast)
Memory Usage:
- Pending requests: ~1KB per request
- Maximum concurrent: 10 requests
- Total memory: < 100KB
Document Status: ✅ Complete and Current
Last Updated: November 15, 2025
Version: 3.0.0+
Maintenance: Review after signing system changes or security audits