{"id":1241,"date":"2025-12-18T14:34:24","date_gmt":"2025-12-18T12:34:24","guid":{"rendered":"https:\/\/ackee.xyz\/blog\/?p=1241"},"modified":"2025-12-18T14:34:24","modified_gmt":"2025-12-18T12:34:24","slug":"eip-712-encoding-in-wake-without-guesswork","status":"publish","type":"post","link":"https:\/\/ackee.xyz\/blog\/eip-712-encoding-in-wake-without-guesswork\/","title":{"rendered":"EIP-712 Encoding in Wake Without Guesswork"},"content":{"rendered":"<h2>Introduction: Tricks for Testing Type Strings<\/h2>\n<p>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\u2019s <code class=\"codehl\">Struct<\/code> base class exposes <code class=\"codehl\">encode_eip712_type<\/code> and <code class=\"codehl\">encode_eip712_data<\/code> 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 <a href=\"https:\/\/ackee.xyz\/blog\/signing-data-in-wake-raw-structured-and-hash-flows\/\">Signing Data in Wake: Raw, Structured, and Hash Flows<\/a>.<\/p>\n<h2>Testing Context And Preconditions<\/h2>\n<ul>\n<li>Run tests inside the Wake testing framework so <code class=\"codehl\">Struct<\/code>, <code class=\"codehl\">Account<\/code>, and chain fixtures are available.<\/li>\n<li>Use dataclasses that subclass <code class=\"codehl\">Struct<\/code>. Field order and field names must mirror the Solidity structs verified on-chain.<\/li>\n<li>Build an <code class=\"codehl\">Eip712Domain<\/code> that matches the contract exactly. The <code class=\"codehl\">name<\/code>, <code class=\"codehl\">version<\/code>, <code class=\"codehl\">chainId<\/code>, and <code class=\"codehl\">verifyingContract<\/code> values must be identical to the Solidity side.<\/li>\n<li>When debugging failures, print the type string and call traces in tests to quickly spot schema or domain mismatches.<\/li>\n<\/ul>\n<h2>Technical Background: What The Helpers Do<\/h2>\n<p><code class=\"codehl\">encode_eip712_type<\/code> builds the canonical type string, including referenced structs, instead of returning a hash. This visibility lets you inspect ordering and field names while debugging. <code class=\"codehl\">encode_eip712_data<\/code> returns the packed bytes that feed into the final digest. Together, these helpers mirror the Solidity side of <code class=\"codehl\">hashTypedData<\/code> and make it straightforward to assert that tests and contracts use the same preimage, even with nested structs or proxy-based verifying contracts.<\/p>\n<h2>Build A Typed Struct You Can Inspect<\/h2>\n<p>The base <code class=\"codehl\">Struct<\/code> class lives in <code class=\"codehl\">wake.testing<\/code>. Subclass it with dataclasses and mirror the Solidity names exactly. When a Python keyword differs from Solidity, use <code class=\"codehl\">field(metadata={&quot;original_name&quot;: ...})<\/code> to preserve the on-chain schema.<\/p>\n<pre><code class=\"language-python\">from dataclasses import dataclass, field\nfrom wake.testing import *\n\n@dataclass\nclass Person(Struct):\n    name: str\n    wallet: Address\n\n@dataclass\nclass Mail(Struct):\n    from_: Person = field(metadata={&quot;original_name&quot;: &quot;from&quot;})\n    to: Person\n    contents: str\n\nmail = Mail(\n    from_=Person(&quot;Alice&quot;, Address(1)),\n    to=Person(&quot;Bob&quot;, Address(2)),\n    contents=&quot;Hello&quot;,\n)\n\nprint(mail.encode_eip712_type())\n# Mail(Person from,Person to,string contents)Person(string name,address wallet)\n<\/code><\/pre>\n<p>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.<\/p>\n<h2>Hash Typed Data The Same Way As Solidity<\/h2>\n<p>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 <code class=\"codehl\">hashTypedData<\/code>.<\/p>\n<pre><code class=\"language-python\">type_hash = keccak256(mail.encode_eip712_type().encode())\ndata = mail.encode_eip712_data()\n\ntyped_data_hash = keccak256(abi.encode_packed(type_hash, data))\ndomain = Eip712Domain(\n    name=&quot;Mail&quot;,\n    version=&quot;1&quot;,\n    chainId=chain.chain_id,\n    verifyingContract=Address(0x1234),\n)\n\nsignature = Account.new().sign_structured(mail, domain)\n<\/code><\/pre>\n<p>Because <code class=\"codehl\">encode_eip712_data<\/code> 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.<\/p>\n<h2>Cross-Check Against A Contract<\/h2>\n<p>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 <code class=\"codehl\">sign_structured<\/code> helper, and verifies that both match the contract\u2019s <code class=\"codehl\">hashTypedData<\/code> implementation on a base contract.<\/p>\n<pre><code class=\"language-python\">from wake.testing import *\nfrom wake.testing.fuzzing import *\nfrom dataclasses import dataclass, field\n\nfrom pytypes.src.utils.ERC1967Factory import ERC1967Factory\nfrom pytypes.ext.wake_tests.helpers.EIP712Mock import EIP712Mock\n\n@dataclass\nclass Person(Struct):\n    name: str\n    wallet: Address\n\n@dataclass\nclass Mail(Struct):\n    from_: Person = field(metadata={&quot;original_name&quot;: &quot;from&quot;})\n    to: Person\n    contents: str\n\nclass Eip712FuzzTest(FuzzTest):\n    def __init__(self):\n        self._factory = ERC1967Factory.deploy()\n\n    def pre_sequence(self) -&gt; None:\n        self._impl = EIP712Mock.deploy()\n        self._signer = Account.new()\n        self._proxy = EIP712Mock(self._factory.deploy_(self._impl, self._signer).return_value)\n\n    @flow()\n    def sign_flow(self, mail: Mail) -&gt; None:\n        type_hash = keccak256(mail.encode_eip712_type().encode())\n        mail_hash = keccak256(abi.encode_packed(type_hash, mail.encode_eip712_data()))\n\n        for target in (self._impl, self._proxy):\n            manual = self._signer.sign_hash(target.hashTypedData(mail_hash))\n            structured = self._signer.sign_structured(\n                mail,\n                Eip712Domain(\n                    name=target.NAME(),\n                    version=target.VERSION(),\n                    chainId=chain.chain_id,\n                    verifyingContract=target.address,\n                ),\n            )\n            assert manual == structured\n\n@chain.connect()\ndef test_eip712_fuzz():\n    Eip712FuzzTest().run(100, 100000)\n<\/code><\/pre>\n<p>Key points:<\/p>\n<ul>\n<li>The type string is visible, so you can confirm that nested structs are included.<\/li>\n<li>The data bytes match Solidity\u2019s preimage exactly, rather than coming from an opaque helper.<\/li>\n<\/ul>\n<h2>Prevention Techniques For Stable Signatures<\/h2>\n<ul>\n<li>Align domains: <code class=\"codehl\">name<\/code>, <code class=\"codehl\">version<\/code>, <code class=\"codehl\">chainId<\/code>, and <code class=\"codehl\">verifyingContract<\/code> must match the contract\u2019s <code class=\"codehl\">EIP712Domain<\/code> exactly.<\/li>\n<li>Keep names canonical: use <code class=\"codehl\">metadata={&quot;original_name&quot;: ...}<\/code> to prevent Python keywords from silently changing Solidity field names.<\/li>\n<li>Log the type string: print <code class=\"codehl\">encode_eip712_type()<\/code> in failing tests to locate ordering or casing issues quickly.<\/li>\n<li>Avoid blind hashes: prefer <code class=\"codehl\">sign_structured<\/code> unless an external API requires a precomputed digest.<\/li>\n<li>Assert preimages: recompute <code class=\"codehl\">keccak256(abi.encodePacked(typeHash, data))<\/code> in Solidity and assert that it equals Wake\u2019s output before verifying a signature.<\/li>\n<li>Fuzz nested structs: generate combinations of optional or nested fields to catch missing struct definitions in the type string.<\/li>\n<\/ul>\n<h2>Conclusion<\/h2>\n<p>Wake\u2019s <code class=\"codehl\">Struct.encode_eip712_type<\/code> and <code class=\"codehl\">encode_eip712_data<\/code> 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.<\/p>\n<h2>Additional Resources<\/h2>\n<ul>\n<li>Wake docs: Accounts and addresses \u2013 <a href=\"https:\/\/ackee.xyz\/wake\/docs\/latest\/testing-framework\/accounts-and-addresses\/\">https:\/\/ackee.xyz\/wake\/docs\/latest\/testing-framework\/accounts-and-addresses\/<\/a><\/li>\n<li>EIP-712 spec \u2013 <a href=\"https:\/\/eips.ethereum.org\/EIPS\/eip-712\">https:\/\/eips.ethereum.org\/EIPS\/eip-712<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>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\u2019s Struct base class exposes encode_eip712_type and encode_eip712_data so you can see exactly what is being hashed, compare it&hellip;<\/p>\n","protected":false},"author":24,"featured_media":1036,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[61,10,80,103],"tags":[],"class_list":["post-1241","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-education","category-ethereum","category-solidity","category-wake"],"aioseo_notices":[],"featured_image_src":"https:\/\/ackee.xyz\/blog\/wp-content\/uploads\/2025\/04\/Flash-Loan-Reentrancy-Attack-1-600x400.png","featured_image_src_square":"https:\/\/ackee.xyz\/blog\/wp-content\/uploads\/2025\/04\/Flash-Loan-Reentrancy-Attack-1-600x600.png","author_info":{"display_name":"Naoki Yoshida","author_link":"https:\/\/ackee.xyz\/blog\/author\/naoki-yoshida\/"},"_links":{"self":[{"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/posts\/1241","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/users\/24"}],"replies":[{"embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/comments?post=1241"}],"version-history":[{"count":0,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/posts\/1241\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/media\/1036"}],"wp:attachment":[{"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/media?parent=1241"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/categories?post=1241"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/tags?post=1241"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}