Introduction: Tricks for Testing Type Strings
A single typo in a type hash is a medium-severity bug, yet verifying that hash by hand is tedious and error-prone. The most time-consuming part of EIP-712 debugging is figuring out what actually went into the digest. Wake’s Struct base class exposes encode_eip712_type and encode_eip712_data so you can see exactly what is being hashed, compare it directly to Solidity, and confirm that signatures match before deployment. If you need a refresher on raw versus structured signatures, start with Signing Data in Wake: Raw, Structured, and Hash Flows.
Testing Context And Preconditions
- Run tests inside the Wake testing framework so
Struct,Account, and chain fixtures are available. - Use dataclasses that subclass
Struct. Field order and field names must mirror the Solidity structs verified on-chain. - Build an
Eip712Domainthat matches the contract exactly. Thename,version,chainId, andverifyingContractvalues must be identical to the Solidity side. - When debugging failures, print the type string and call traces in tests to quickly spot schema or domain mismatches.
Technical Background: What The Helpers Do
encode_eip712_type builds the canonical type string, including referenced structs, instead of returning a hash. This visibility lets you inspect ordering and field names while debugging. encode_eip712_data returns the packed bytes that feed into the final digest. Together, these helpers mirror the Solidity side of hashTypedData and make it straightforward to assert that tests and contracts use the same preimage, even with nested structs or proxy-based verifying contracts.
Build A Typed Struct You Can Inspect
The base Struct class lives in wake.testing. Subclass it with dataclasses and mirror the Solidity names exactly. When a Python keyword differs from Solidity, use field(metadata={"original_name": ...}) to preserve the on-chain schema.
from dataclasses import dataclass, field
from wake.testing import *
@dataclass
class Person(Struct):
name: str
wallet: Address
@dataclass
class Mail(Struct):
from_: Person = field(metadata={"original_name": "from"})
to: Person
contents: str
mail = Mail(
from_=Person("Alice", Address(1)),
to=Person("Bob", Address(2)),
contents="Hello",
)
print(mail.encode_eip712_type())
# Mail(Person from,Person to,string contents)Person(string name,address wallet)
Keeping the type string human-readable helps you spot mismatches early, such as incorrect data types, stray spaces, missing nested structs, or renamed variables. Once the string matches the Solidity side, you can safely hash it.
Hash Typed Data The Same Way As Solidity
With the type string verified, build the type hash and data bytes exactly as the contract does. The snippet below mirrors the EIP-712 pipeline used inside hashTypedData.
type_hash = keccak256(mail.encode_eip712_type().encode())
data = mail.encode_eip712_data()
typed_data_hash = keccak256(abi.encode_packed(type_hash, data))
domain = Eip712Domain(
name="Mail",
version="1",
chainId=chain.chain_id,
verifyingContract=Address(0x1234),
)
signature = Account.new().sign_structured(mail, domain)
Because encode_eip712_data returns the exact bytes that feed into the digest, you can log and compare them against a Solidity helper or a live contract call without guessing.
Cross-Check Against A Contract
End-to-end assertions provide the strongest proof. The fuzz test below signs the same struct in two ways, using a manual hash and the high-level sign_structured helper, and verifies that both match the contract’s hashTypedData implementation on a base contract.
from wake.testing import *
from wake.testing.fuzzing import *
from dataclasses import dataclass, field
from pytypes.src.utils.ERC1967Factory import ERC1967Factory
from pytypes.ext.wake_tests.helpers.EIP712Mock import EIP712Mock
@dataclass
class Person(Struct):
name: str
wallet: Address
@dataclass
class Mail(Struct):
from_: Person = field(metadata={"original_name": "from"})
to: Person
contents: str
class Eip712FuzzTest(FuzzTest):
def __init__(self):
self._factory = ERC1967Factory.deploy()
def pre_sequence(self) -> None:
self._impl = EIP712Mock.deploy()
self._signer = Account.new()
self._proxy = EIP712Mock(self._factory.deploy_(self._impl, self._signer).return_value)
@flow()
def sign_flow(self, mail: Mail) -> None:
type_hash = keccak256(mail.encode_eip712_type().encode())
mail_hash = keccak256(abi.encode_packed(type_hash, mail.encode_eip712_data()))
for target in (self._impl, self._proxy):
manual = self._signer.sign_hash(target.hashTypedData(mail_hash))
structured = self._signer.sign_structured(
mail,
Eip712Domain(
name=target.NAME(),
version=target.VERSION(),
chainId=chain.chain_id,
verifyingContract=target.address,
),
)
assert manual == structured
@chain.connect()
def test_eip712_fuzz():
Eip712FuzzTest().run(100, 100000)
Key points:
- The type string is visible, so you can confirm that nested structs are included.
- The data bytes match Solidity’s preimage exactly, rather than coming from an opaque helper.
Prevention Techniques For Stable Signatures
- Align domains:
name,version,chainId, andverifyingContractmust match the contract’sEIP712Domainexactly. - Keep names canonical: use
metadata={"original_name": ...}to prevent Python keywords from silently changing Solidity field names. - Log the type string: print
encode_eip712_type()in failing tests to locate ordering or casing issues quickly. - Avoid blind hashes: prefer
sign_structuredunless an external API requires a precomputed digest. - Assert preimages: recompute
keccak256(abi.encodePacked(typeHash, data))in Solidity and assert that it equals Wake’s output before verifying a signature. - Fuzz nested structs: generate combinations of optional or nested fields to catch missing struct definitions in the type string.
Conclusion
Wake’s Struct.encode_eip712_type and encode_eip712_data remove guesswork from typed-data tests. By exposing the type string and preimage bytes, they let you align precisely with Solidity, validate contract domains, and prove that signatures match without trial and error. Keep domains consistent, log type strings, and assert digests on both sides to ship reliable permit and meta-transaction flows.
Additional Resources
- Wake docs: Accounts and addresses – https://ackee.xyz/wake/docs/latest/testing-framework/accounts-and-addresses/
- EIP-712 spec – https://eips.ethereum.org/EIPS/eip-712