Support EIP-712 cheatcodes in Solidity Test#8243
Conversation
🦋 Changeset detectedLatest commit: 382df09 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
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 |
…rdhat into EIP-712-cheatcodes-hh
…Foundation/hardhat into EIP-712-cheatcodes-hh
There was a problem hiding this comment.
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
eip712Typesconfig (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. |
| */ | ||
| export function canonicalizeStructs( | ||
| structs: CollectedStruct[], | ||
| selectedNames?: Set<string>, |
There was a problem hiding this comment.
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.
| 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; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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;
}
}
}
schaable
left a comment
There was a problem hiding this comment.
Left one final comment, but I'm pre-approving. Thanks for bearing with me while I reviewed this 😅
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) andcollectEip712CanonicalTypesboth JSON.parse the same build-info outputs from buildInfosAndOutputs. We could cache the parsed outputs ingetTestFunctionOverridesand reuse them fromcollectEip712CanonicalTypesto 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
S_0andS_1; user callsvm.eip712HashType("S_0").EIP712_DUPLICATE_STRUCT_NAMEand 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-jsonbyte-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.
Expected:
vm.eip712HashType("Node")returnskeccak256("Node(uint256 value,Node[] children)").Actual: the whole test run aborts before any test executes:
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.
eip712HashStructsilently rejects the tuple ABI encodingabi.encode(field1, field2, ...)andabi.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.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.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.