-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Description
Component
Cast
Have you ensured that all of these are up to date?
- FoundryFoundryupTo pick up a draggable item, press the space bar. While dragging, use the arrow keys to move the item. Press space again to drop the item in its new position, or press escape to cancel.
What version of Foundry are you on?
forge Version: 1.2.3-stable Commit SHA: a813a2c Build Timestamp: 2025-06-08T15:42:50.507050000Z (1749397370) Build Profile: maxperf
What version of Foundryup are you on?
foundryup: 1.1.0
What command(s) is the bug in?
cast wallet sign
Operating System
None
Describe the bug
Cast EIP-712 Type Name Colon Bug Report
Summary
cast wallet sign --data
fails to parse valid EIP-712 structured data when type names contain colons (:
) characters, even though colons are permitted in EIP-712 type names according to the specification.
Bug Details
Error Message:
Error: failed to parse json file: data did not match any variant of untagged enum StrOrVal
Affected Command:
cast wallet sign --private-key <key> --data --from-file <eip712.json>
Expected Behavior
Cast should successfully parse and sign EIP-712 data with type names containing colons, as these are valid according to the EIP-712 specification.
Actual Behavior
Cast fails with a parsing error when EIP-712 type names contain colons.
Proof of Concept
Working Case (No Colon)
{
"types": {
"EIP712Domain": [...],
"TestMessage": [
{"name": "content", "type": "string"}
]
},
"primaryType": "TestMessage",
...
}
✅ Result: Cast successfully signs this data
Failing Case (With Colon)
{
"types": {
"EIP712Domain": [...],
"Test:Message": [
{"name": "content", "type": "string"}
]
},
"primaryType": "Test:Message",
...
}
❌ Result: Cast fails with parsing error
Real-World Impact
This bug affects protocols that use colons in their EIP-712 type names, such as:
- Hyperliquid (uses types like
HyperliquidTransaction:UsdSend
,HyperliquidTransaction:CDeposit
) - Other protocols following similar naming conventions
Verification
The Python eth_account
library correctly handles both cases, proving that colons in EIP-712 type names are valid:
from eth_account.messages import encode_typed_data
from eth_account import Account
# Both of these work correctly with eth_account:
structured_data_1 = encode_typed_data(full_message=data_without_colon)
structured_data_2 = encode_typed_data(full_message=data_with_colon)
wallet = Account.create()
signature_1 = wallet.sign_message(structured_data_1) # ✅ Works
signature_2 = wallet.sign_message(structured_data_2) # ✅ Works
Reproduction Steps
- Create a simple EIP-712 structure with a colon in the type name
- Try to sign it with
cast wallet sign --data --from-file
- Observe the parsing failure
- Remove the colon and try again - it will work
Here is an example script that demonstrates the issue. This script signs 2 bodies with colons, and 2 without colons. eth_account
succeeds for all four, while cast fails for the bodies with colons.
#!/usr/bin/env python3
"""
Minimal debug to find exactly what's breaking cast
"""
import json
import subprocess
import tempfile
from eth_account import Account
from eth_account.messages import encode_typed_data
def test_cast_variations():
"""Test different variations to isolate the issue"""
wallet = Account.create()
print(f"Test wallet: {wallet.address}")
print()
# Test 1: Very simple structure (should work)
simple = {
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"TestMessage": [
{"name": "content", "type": "string"}
]
},
"primaryType": "TestMessage",
"domain": {
"name": "Test",
"version": "1",
"chainId": 1,
"verifyingContract": "0x0000000000000000000000000000000000000000"
},
"message": {
"content": "hello"
}
}
# Test 2: Same but with colon in type name
with_colon = {
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"Test:Message": [
{"name": "content", "type": "string"}
]
},
"primaryType": "Test:Message",
"domain": {
"name": "Test",
"version": "1",
"chainId": 1,
"verifyingContract": "0x0000000000000000000000000000000000000000"
},
"message": {
"content": "hello"
}
}
# Test 3: Hyperliquid domain but simple message
hyperliquid_domain = {
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"Message": [
{"name": "content", "type": "string"}
]
},
"primaryType": "Message",
"domain": {
"name": "HyperliquidSignTransaction",
"version": "1",
"chainId": 421614,
"verifyingContract": "0x0000000000000000000000000000000000000000"
},
"message": {
"content": "hello"
}
}
# Test 4: Hyperliquid domain with colon type
hyperliquid_colon = {
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"Test:Message": [
{"name": "content", "type": "string"}
]
},
"primaryType": "Test:Message",
"domain": {
"name": "HyperliquidSignTransaction",
"version": "1",
"chainId": 421614,
"verifyingContract": "0x0000000000000000000000000000000000000000"
},
"message": {
"content": "hello"
}
}
tests = [
(simple, "Simple structure"),
(with_colon, "With colon in type name"),
(hyperliquid_domain, "Hyperliquid domain, simple message"),
(hyperliquid_colon, "Hyperliquid domain with colon type")
]
# Test cast signing
print("CAST WALLET SIGN TESTS:")
print("=" * 50)
for data, name in tests:
test_cast_signing(data, name, wallet)
# Test eth_account signing
print("\nETH_ACCOUNT SIGNING TESTS:")
print("=" * 50)
test_eth_account_signing(tests, wallet)
def test_cast_signing(data, name, wallet):
"""Test cast wallet sign with given data"""
print(f"=== {name} ===")
# Use a permanent file instead of temporary
filename = f"test_data_{name.lower().replace(' ', '_').replace(',', '')}.json"
print(f"Writing to {filename}")
with open(filename, 'w') as f:
json.dump(data, f, indent=2)
try:
result = subprocess.run([
'cast', 'wallet', 'sign',
'--private-key', wallet.key.hex(),
'--data',
'--from-file',
filename
], capture_output=True, text=True)
if result.returncode == 0:
signature_hex = result.stdout.strip()
print(f"✅ SUCCESS: Cast signed successfully")
print(f" Raw signature: {signature_hex}")
# Decode the signature (cast returns 0x + 64 bytes r + 64 bytes s + 1 byte v)
if signature_hex.startswith('0x'):
sig_data = signature_hex[2:]
else:
sig_data = signature_hex
if len(sig_data) == 130: # 64 + 64 + 2 hex chars
r_hex = sig_data[:64]
s_hex = sig_data[64:128]
v_hex = sig_data[128:130]
print(f" r: 0x{r_hex}")
print(f" s: 0x{s_hex}")
print(f" v: {int(v_hex, 16)}")
else:
print(f" ⚠️ Unexpected signature length: {len(sig_data)} chars")
else:
print(f"❌ FAILED: {result.stderr.strip()}")
except Exception as e:
print(f"❌ EXCEPTION: {e}")
print()
def test_eth_account_signing(tests, wallet):
"""Test eth_account signing with the same data structures"""
print("Testing eth_account library with the same EIP-712 structures...")
print("(This demonstrates that the data structures are valid)")
print()
for data, name in tests:
print(f"=== {name} ===")
try:
# Use eth_account to encode and sign the typed data
structured_data = encode_typed_data(full_message=data)
signed_message = wallet.sign_message(structured_data)
print(f"✅ SUCCESS: eth_account signed successfully")
print(f" Message hash: {signed_message.message_hash.hex()}")
print(f" Signature: {signed_message.signature.hex()}")
print(f" r: {hex(signed_message.r)}")
print(f" s: {hex(signed_message.s)}")
print(f" v: {signed_message.v}")
except Exception as e:
print(f"❌ FAILED: {e}")
print()
print("CONCLUSION:")
print("- eth_account successfully handles ALL test cases, including those with colons")
print("- This proves the EIP-712 structures are valid according to the specification")
print("- Cast's failure with colons is a bug in cast's JSON parser, not the data")
if __name__ == "__main__":
test_cast_variations()
Environment
- Cast version: 0.2.0 (git: 8b1c9b2, book: 68bf1dc)
- OS: macOS
- Python: 3.9+
- eth_account: 0.8.0+
Proposed Fix
The cast EIP-712 parser should be updated to accept colons in type names, as they are valid characters according to the EIP-712 specification.
Workaround
For now, protocols can work around this by:
- Replacing colons with underscores in type names when using cast
- Ensuring the replacement is consistent across primaryType and types definitions
- Note: This changes the signature hash, so it's not compatible with existing signatures
References
- EIP-712 Specification
- eth_account library (reference implementation that works correctly)
Metadata
Metadata
Assignees
Labels
Type
Projects
Status
Activity
addiaddiaddi commentedon Jun 12, 2025
Reproduction script response:
test(cast): add tests for EIP-712 type names with colons
:
alloy-rs/core#964prestwich commentedon Jun 13, 2025
the spec defines them to be valid identifiers, and the identifier grammar rule does not allow
:
, so according to the 712 spec, colons are illegalalloy-rs/core#963 (comment)
0xrusowsky commentedon Jun 16, 2025
despite Hyperliquid namespacing using
:
and not being compliant with the EIP712 spec, we decided to support it.see the relevant discussion in the PR alloy-rs/core#964
4 remaining items