Skip to content

Cast Wallet Sign Does not work when colons are in type name #10765

@addiaddiaddi

Description

@addiaddiaddi

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

  1. Create a simple EIP-712 structure with a colon in the type name
  2. Try to sign it with cast wallet sign --data --from-file
  3. Observe the parsing failure
  4. 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:

  1. Replacing colons with underscores in type names when using cast
  2. Ensuring the replacement is consistent across primaryType and types definitions
  3. Note: This changes the signature hash, so it's not compatible with existing signatures

References

Activity

addiaddiaddi

addiaddiaddi commented on Jun 12, 2025

@addiaddiaddi
Author

Reproduction script response:

python3 cast_debug_minimal.py
Test wallet: 0x35fAd9e359d802Dd638aCd336963a9d06a11998F

CAST WALLET SIGN TESTS:
==================================================
=== Simple structure ===
Writing to test_data_simple_structure.json
✅ SUCCESS: Cast signed successfully
   Raw signature: 0x6e2f612df52e1b405438333364f592b1e7bbebe210db7242b9df4866183b9ad831a7ecfc209cef9153c56c841efe6e5902530e2bbbe3a894e8c76f73563d083d1c
   r: 0x6e2f612df52e1b405438333364f592b1e7bbebe210db7242b9df4866183b9ad8
   s: 0x31a7ecfc209cef9153c56c841efe6e5902530e2bbbe3a894e8c76f73563d083d
   v: 28

=== With colon in type name ===
Writing to test_data_with_colon_in_type_name.json
❌ FAILED: Error: failed to parse json file: "test_data_with_colon_in_type_name.json": data did not match any variant of untagged enum StrOrVal

=== Hyperliquid domain, simple message ===
Writing to test_data_hyperliquid_domain_simple_message.json
✅ SUCCESS: Cast signed successfully
   Raw signature: 0x8e449db77851a0312fbc1d9c70b7690bd46f0d9c78e554d16d7db1063a26ab99338babc535c557fb2670413d5897fa2e61da3e2924d555b2c0a22d628a8247a41c
   r: 0x8e449db77851a0312fbc1d9c70b7690bd46f0d9c78e554d16d7db1063a26ab99
   s: 0x338babc535c557fb2670413d5897fa2e61da3e2924d555b2c0a22d628a8247a4
   v: 28

=== Hyperliquid domain with colon type ===
Writing to test_data_hyperliquid_domain_with_colon_type.json
❌ FAILED: Error: failed to parse json file: "test_data_hyperliquid_domain_with_colon_type.json": data did not match any variant of untagged enum StrOrVal


ETH_ACCOUNT SIGNING TESTS:
==================================================
Testing eth_account library with the same EIP-712 structures...
(This demonstrates that the data structures are valid)

=== Simple structure ===
✅ SUCCESS: eth_account signed successfully
   Message hash: 7e7453ea342b211f1f96229eac54106a87e1d9304f059202c27d298d2a6366da
   Signature: 6e2f612df52e1b405438333364f592b1e7bbebe210db7242b9df4866183b9ad831a7ecfc209cef9153c56c841efe6e5902530e2bbbe3a894e8c76f73563d083d1c
   r: 0x6e2f612df52e1b405438333364f592b1e7bbebe210db7242b9df4866183b9ad8
   s: 0x31a7ecfc209cef9153c56c841efe6e5902530e2bbbe3a894e8c76f73563d083d
   v: 28

=== With colon in type name ===
✅ SUCCESS: eth_account signed successfully
   Message hash: 6865f9ca461056e5577002f33bb1d7cecfa52ad34a8c6dc285ce2fda51d9365b
   Signature: 7cadacefd91da3243d6a1387f7af7049005191040758f38af05d69077f94d6890d2f0a56efaba854367fcf9324c7ff19a9ea106dd015d8ca7395acf865f55fe01c
   r: 0x7cadacefd91da3243d6a1387f7af7049005191040758f38af05d69077f94d689
   s: 0xd2f0a56efaba854367fcf9324c7ff19a9ea106dd015d8ca7395acf865f55fe0
   v: 28

=== Hyperliquid domain, simple message ===
✅ SUCCESS: eth_account signed successfully
   Message hash: 3396a9a1a199ada5b73f019360b6a14ccb34b4055987b7a8484e45b672c9327b
   Signature: 8e449db77851a0312fbc1d9c70b7690bd46f0d9c78e554d16d7db1063a26ab99338babc535c557fb2670413d5897fa2e61da3e2924d555b2c0a22d628a8247a41c
   r: 0x8e449db77851a0312fbc1d9c70b7690bd46f0d9c78e554d16d7db1063a26ab99
   s: 0x338babc535c557fb2670413d5897fa2e61da3e2924d555b2c0a22d628a8247a4
   v: 28

=== Hyperliquid domain with colon type ===
✅ SUCCESS: eth_account signed successfully
   Message hash: 45f2dd63abcc6e512fff7a8234cca0256db9f91e8f00a74caf814739b75b86c7
   Signature: 07cdbf8e6f389fad1a788347dfc3712ed69a8e94ec206bea6faca7fb8360479c3d4bdaf268714e26cc687cb526068fc5f332d8805ab9cc0dbbd97f4922941f111c
   r: 0x7cdbf8e6f389fad1a788347dfc3712ed69a8e94ec206bea6faca7fb8360479c
   s: 0x3d4bdaf268714e26cc687cb526068fc5f332d8805ab9cc0dbbd97f4922941f11
   v: 28

CONCLUSION:
- eth_account successfully handles ALL test cases, including those with colons
- This proves the EIP-712 structures are valid according to the specification
- Cast's failure with colons is a bug in cast's JSON parser, not the data
added a commit that references this issue on Jun 13, 2025
6f76667
added
C-castCommand: cast
and removed
T-needs-triageType: this issue needs to be labelled
on Jun 13, 2025
moved this from Backlog to In Progress in Foundryon Jun 13, 2025
prestwich

prestwich commented on Jun 13, 2025

@prestwich
Contributor

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.

the spec defines them to be valid identifiers, and the identifier grammar rule does not allow :, so according to the 712 spec, colons are illegal

alloy-rs/core#963 (comment)

0xrusowsky

0xrusowsky commented on Jun 16, 2025

@0xrusowsky
Contributor

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

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

C-castCommand: castT-bugType: bug

Type

No type

Projects

Status

Completed

Milestone

No milestone

Relationships

None yet

    Development

    Participants

    @prestwich@mattsse@addiaddiaddi@0xrusowsky@zerosnacks

    Issue actions

      Cast Wallet Sign Does not work when colons are in type name · Issue #10765 · foundry-rs/foundry