Skip to content

Support EIP-712 cheatcodes in Solidity Test#8243

Merged
ChristopherDedominici merged 40 commits into
mainfrom
EIP-712-cheatcodes-hh
May 15, 2026
Merged

Support EIP-712 cheatcodes in Solidity Test#8243
ChristopherDedominici merged 40 commits into
mainfrom
EIP-712-cheatcodes-hh

Conversation

@ChristopherDedominici
Copy link
Copy Markdown
Contributor

@ChristopherDedominici ChristopherDedominici commented May 5, 2026

Link to website docs PR: NomicFoundation/hardhat-website#260

Fixes: #8171


Possible follow-up

See comment here - the comment is partially addressed here

getTestFunctionOverrides (packages/hardhat/src/internal/builtin-plugins/solidity-test/inline-config/index.ts) and collectEip712CanonicalTypes both JSON.parse the same build-info outputs from buildInfosAndOutputs. We could cache the parsed outputs in getTestFunctionOverrides and reuse them from collectEip712CanonicalTypes to avoid re-parsing the overlap.


Question

✅ Answer from EDR team: throwing makes sense: see here

The PR can already be merged, but I have 1open question that can be addressed now, as follow-up, or ignored if we think the solution is solid enough.

Duplicate struct names throw instead of being disambiguated

// File A.sol
struct S { uint256 a; }

// File B.sol
struct S { uint256 b; }
  • Forge: silently renames to S_0 and S_1; user calls vm.eip712HashType("S_0").
  • This PR: throws EIP712_DUPLICATE_STRUCT_NAME and refuses to run.

The new behavior is arguably better UX, but it's a divergence — projects that worked on forge will fail here. Worth flagging in the migration guide.


Potential EDR issues

Tracked here

Three issues rooted in EDR. Hardhat's collector emits canonical type strings that match forge bind-json byte-for-byte; EDR's downstream handling diverges.

1. Self-referential structs crash the test run

A struct that references itself (directly or transitively) makes EDR abort at startup.

struct Node {
  uint256 value;
  Node[] children;
}

Expected: vm.eip712HashType("Node") returns keccak256("Node(uint256 value,Node[] children)").

Actual: the whole test run aborts before any test executes:

HHE802: Unhandled EDR error while running Solidity tests:
failed to canonicalize EIP-712 type Node(uint256 value,Node[] children):
missing type in type resolution: primary component

Root cause: EDR's canonical-type resolver expects every member type to appear in the appended-dependency section. For a self-reference, the type appears only as the primary, not in the appended section, and resolution fails.

Impact: anyone with a tree, linked list, or recursive routing struct — common in DeFi — hits this on first run. The only workaround is to rename or remove the struct from the include glob.

2. eip712HashStruct silently rejects the tuple ABI encoding

abi.encode(field1, field2, ...) and abi.encode(structInstance) are byte-identical for static-only structs, but differ for any struct with a dynamic field (string, bytes, array). EDR only accepts the second form.

struct Sample {
  address a;
  string memo;
}

// Works:
CHEATS.eip712HashStruct(
  "Sample",
  abi.encode(Sample({a: address(0xCAFE), memo: "hi"}))
);

// Fails with "buffer overrun while deserializing":
CHEATS.eip712HashStruct(
  "Sample",
  abi.encode(address(0xCAFE), "hi")
);

Root cause: EDR's cheatcode handler ABI-decodes the buffer as the resolved struct type, which includes the outer offset word for dynamic structs. The tuple form lacks that word, so decoding overruns.

Impact: users will write the tuple form first (it works for static structs and reads more naturally), then hit a confusing error the moment they add a string or bytes field. The error message doesn't mention either encoding form.

Suggested fix: accept both shapes, or replace the error message with something like expected abi.encode(structInstance); got abi.encode(field1, field2, ...) — these are not equivalent for structs with dynamic fields.

3. Whitespace in the canonical-type-string overload is rejected

The design doc says vm.eip712HashType "will output the canonical type even if the input is malformed with extra whitespaces". EDR's parser does not.

// Works:
CHEATS.eip712HashType("Foo(uint256 x)");

// Fails:
CHEATS.eip712HashType("Foo( uint256 x )");
//   vm.eip712HashType: failed to parse EIP-712 canonical type
//   Foo( uint256 x ): no parseable type definition found

Root cause: EDR's canonical-type-string parser tokenizes strictly and doesn't normalize whitespace around (, ), or ,.

Impact: divergence from the documented behavior. Forge handles this; users porting test fixtures from Foundry can hit it.

Suggested fix: normalize whitespace in the parser before tokenizing.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 5, 2026

🦋 Changeset detected

Latest commit: 382df09

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@nomicfoundation/hardhat-errors Patch
hardhat Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ChristopherDedominici ChristopherDedominici linked an issue May 5, 2026 that may be closed by this pull request
@ChristopherDedominici ChristopherDedominici moved this from Backlog to In Progress in Hardhat May 5, 2026
@ChristopherDedominici ChristopherDedominici marked this pull request as ready for review May 5, 2026 15:31
@ChristopherDedominici ChristopherDedominici moved this from In Progress to In Review in Hardhat May 6, 2026
Base automatically changed from build/bump-edr-0.12.0-next.32 to main May 6, 2026 13:45
…Foundation/hardhat into EIP-712-cheatcodes-hh
Copilot AI review requested due to automatic review settings May 7, 2026 12:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds opt-in support for EIP-712 cheatcodes in Hardhat Solidity Test by collecting EIP-712 canonical type strings from compiled Solidity ASTs (scoped by include/exclude globs) and passing them into the EDR Solidity test runner. This aligns Hardhat’s Solidity Test integration with EDR’s new EIP-712 cheatcode support.

Changes:

  • Introduces an eip712Types config (include/exclude globs) and wires it through the Solidity test task.
  • Implements AST walking + canonicalization to produce EIP-712 canonical type strings, plus glob matching utilities.
  • Adds unit tests for glob selection, AST extraction/type encoding, canonicalization rules, and multi-build-info orchestration; adds a new Hardhat error for duplicate struct-name conflicts.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/hardhat/src/internal/builtin-plugins/solidity-test/task-action.ts Collects EIP-712 canonical types and passes them to the runner config builder.
packages/hardhat/src/internal/builtin-plugins/solidity-test/helpers.ts Extends runner-config conversion to accept eip712CanonicalTypes and prevents passing eip712Types through the generic config object.
packages/hardhat/src/internal/builtin-plugins/solidity-test/type-extensions.ts Adds eip712Types to Solidity test user config typing.
packages/hardhat/src/internal/builtin-plugins/solidity-test/config.ts Extends Zod config validation to accept eip712Types.include/exclude.
packages/hardhat/src/internal/builtin-plugins/solidity-test/eip712/index.ts Orchestrates parsing build infos, filtering by globs, extracting structs, and producing canonical EIP-712 type strings.
packages/hardhat/src/internal/builtin-plugins/solidity-test/eip712/ast-walker.ts Extracts struct definitions from solc AST and encodes member types into EIP-712-compatible type strings.
packages/hardhat/src/internal/builtin-plugins/solidity-test/eip712/canonicalize.ts Canonicalizes structs into EIP-712 encodeType strings and enforces duplicate-name constraints.
packages/hardhat/src/internal/builtin-plugins/solidity-test/eip712/glob.ts Implements include/exclude glob matching for user source paths.
packages/hardhat/test/internal/builtin-plugins/solidity-test/eip712/index.ts Tests build-info orchestration, include/exclude filtering, dedupe behavior, and build-info mapping resolution.
packages/hardhat/test/internal/builtin-plugins/solidity-test/eip712/ast-walker.ts Unit tests for AST struct extraction and member type encoding.
packages/hardhat/test/internal/builtin-plugins/solidity-test/eip712/canonicalize.ts Unit tests for EIP-712 canonicalization behavior and duplicate-name erroring.
packages/hardhat/test/internal/builtin-plugins/solidity-test/eip712/glob.ts Unit tests for glob selection semantics and pattern behavior.
packages/hardhat-errors/src/descriptors.ts Adds EIP712_DUPLICATE_STRUCT_NAME error descriptor and messaging.
.changeset/good-islands-hide.md Changeset for releasing the new feature.

Comment thread packages/hardhat/src/internal/builtin-plugins/solidity-test/eip712/index.ts Outdated
Comment thread packages/hardhat/src/internal/builtin-plugins/solidity-test/eip712/glob.ts Outdated
@ChristopherDedominici ChristopherDedominici moved this from In Review to In Progress in Hardhat May 7, 2026
Copilot AI review requested due to automatic review settings May 7, 2026 13:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Comment thread packages/hardhat/src/internal/builtin-plugins/solidity-test/eip712/index.ts Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings May 15, 2026 10:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.

*/
export function canonicalizeStructs(
structs: CollectedStruct[],
selectedNames?: Set<string>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selectedNames is always passed from collectEip712CanonicalTypes, so the optional signature and the undefined branches only exist to simplify tests. I'd make it required and have tests pass new Set(collected.map((s) => s.name)), or use a small helper, for the "emit everything" case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here: b100ffd

Copilot AI review requested due to automatic review settings May 15, 2026 13:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings May 15, 2026 14:34
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.

Comment on lines +89 to +103
while (changed) {
changed = false;
for (const name of [...encodable]) {
const struct = byName.get(name);
if (struct === undefined) {
continue;
}

const deps = directStructDeps(struct, knownNames);
if (deps.some((dep) => !encodable.has(dep))) {
encodable.delete(name);
changed = true;
}
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block recomputes the deps on every iteration, but the deps are always the same for a given struct. We could precompute them to reduce the complexity, something like:

  const depsByName = new Map<string, string[]>();
  for (const [name, struct] of byName) {
    depsByName.set(name, directStructDeps(struct, knownNames));
  }
  
  let changed = true;
  while (changed) {
    changed = false;
    for (const name of [...encodable]) {
      const deps = depsByName.get(name);
      assertHardhatInvariant(deps !== undefined, "...");
      if (deps.some((dep) => !encodable.has(dep))) {
        encodable.delete(name);
        changed = true;
      }
    }
  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here: 382df09

Copy link
Copy Markdown
Member

@schaable schaable left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left one final comment, but I'm pre-approving. Thanks for bearing with me while I reviewed this 😅

@ChristopherDedominici ChristopherDedominici added this pull request to the merge queue May 15, 2026
Merged via the queue into main with commit a847483 May 15, 2026
402 of 404 checks passed
@ChristopherDedominici ChristopherDedominici deleted the EIP-712-cheatcodes-hh branch May 15, 2026 15:34
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Hardhat May 15, 2026
@github-actions github-actions Bot mentioned this pull request May 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Support EIP-712 cheatcodes in Solidity Test

5 participants