Description
Component
Cast
Have you ensured that all of these are up to date?
- Foundry
- Foundryup
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