-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Support EIP-712 cheatcodes in Solidity Test #8243
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
40 commits
Select commit
Hold shift + click to select a range
f458b58
logic + tests
ChristopherDedominici 08805db
Create good-islands-hide.md
ChristopherDedominici 9473d06
fix spelling issues
ChristopherDedominici 9d6f57f
Merge branch 'EIP-712-cheatcodes-hh' of github.com:NomicFoundation/ha…
ChristopherDedominici 433a986
Merge branches 'EIP-712-cheatcodes-hh' and 'main' of github.com:Nomic…
ChristopherDedominici 061f5e7
add link to website docs
ChristopherDedominici a5d4090
cache the glob results
ChristopherDedominici 96a94d7
fix EIP-712 collector pre-dedupe hiding same-file struct conflicts
ChristopherDedominici 7a56f67
fix EIP-712 indexByName dropping encodable structs on unsupported-mem…
ChristopherDedominici 692c71e
Potential fix for pull request finding
ChristopherDedominici c71f117
fix EIP-712 collector dropping transitive project files from include …
ChristopherDedominici 5d7d293
Merge branch 'main' of github.com:NomicFoundation/hardhat into EIP-71…
ChristopherDedominici 38068f7
Merge branch 'EIP-712-cheatcodes-hh' of github.com:NomicFoundation/ha…
ChristopherDedominici dcf0646
test EIP-712 glob matcher against npm-rooted source names
ChristopherDedominici a187b09
UDVTs resolves underlying types
ChristopherDedominici ab493ab
support `supports {a,b} and [abc]` in glob
ChristopherDedominici 49ec428
Merge branch 'main' of github.com:NomicFoundation/hardhat into EIP-71…
ChristopherDedominici 370a227
lint:fix
ChristopherDedominici f2cdddd
split changeset file
ChristopherDedominici 4fecd9e
fix grammar errors
ChristopherDedominici c781261
fix glob bug
ChristopherDedominici c47fe34
cover empty `[]` and `[!]` char classes in glob tests
ChristopherDedominici fc6e36e
Merge branch 'EIP-712-cheatcodes-hh' of github.com:NomicFoundation/ha…
ChristopherDedominici 2933352
fix spelling issues
ChristopherDedominici a17ad59
inline cross-file deps when filtering EIP-712 structs
ChristopherDedominici e043b88
throw on conflicting non-selected EIP-712 struct deps
ChristopherDedominici 21822a4
move constant
ChristopherDedominici 9fc53e1
move eip712Types destructure to task-action boundary
ChristopherDedominici 1feac9d
simplify comment
ChristopherDedominici 3e7fef7
resolve eip712Types defaults at config resolution
ChristopherDedominici f545768
return undefined for unresolvable array length
ChristopherDedominici 883477f
normalize EIP-712 elementary type aliases via typeString
ChristopherDedominici 27191bf
remove redundant Literal branch in array length encoding
ChristopherDedominici 76523d5
remove dead interface branch in EIP-712 type encoder
ChristopherDedominici 62cf8b9
skip parsing build infos that can't define EIP-712 types
ChristopherDedominici 6902946
treat missing array length as dynamic in EIP-712 walker
ChristopherDedominici b100ffd
require selectedNames in EIP-712 canonicalizer
ChristopherDedominici 1101e0c
simplify array-suffix stripping in EIP-712 dep walker
ChristopherDedominici cf58e3a
remove redundant canonical-string dedup in EIP-712 canonicalizer
ChristopherDedominici 382df09
precompute struct deps in EIP-712 encodable loop
ChristopherDedominici File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| # docs: https://github.com/NomicFoundation/hardhat-website/pull/260 | ||
| "@nomicfoundation/hardhat-errors": patch | ||
| --- | ||
|
|
||
| Add error descriptor for duplicate EIP-712 struct names surfaced by the Solidity Test cheatcodes. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| # docs: https://github.com/NomicFoundation/hardhat-website/pull/260 | ||
| "hardhat": patch | ||
| --- | ||
|
|
||
| Support EIP-712 cheatcodes in Solidity Test. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
324 changes: 324 additions & 0 deletions
324
packages/hardhat/src/internal/builtin-plugins/solidity-test/eip712/ast-walker.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,324 @@ | ||
| import { isObject } from "@nomicfoundation/hardhat-utils/lang"; | ||
|
|
||
| /** | ||
| * A single field of a Solidity struct | ||
| */ | ||
| export interface StructMember { | ||
| name: string; | ||
| /** | ||
| * EIP-712 type string, or `undefined` if the type can't be encoded (e.g. mappings). | ||
| */ | ||
| type: string | undefined; | ||
| } | ||
|
|
||
| /** | ||
| * A Solidity struct extracted from a source AST (Abstract Syntax Tree). | ||
| */ | ||
| export interface CollectedStruct { | ||
| name: string; | ||
| members: StructMember[]; | ||
| /** | ||
| * Project-relative source path. Used for diagnostics only. | ||
| */ | ||
| sourcePath: string; | ||
| } | ||
|
|
||
| /** | ||
| * A user-defined value type (`type Foo is bytes32;`) resolves to its | ||
| * underlying elementary type for EIP-712 encoding. | ||
| * The map is keyed by the user-defined value type definition's solc node id, | ||
| * which is what `referencedDeclaration` on a `UserDefinedTypeName` reference | ||
| * points to. | ||
| */ | ||
| export type UserDefinedValueTypeIndex = Map<number, Record<string, unknown>>; | ||
|
|
||
| /** | ||
| * Walks every AST in the build and indexes every `UserDefinedValueTypeDefinition` | ||
| * by its node `id`, mapping to its `underlyingType` (an `ElementaryTypeName`). | ||
| * | ||
| * User-defined value types can sit at file scope or inside a contract, and a | ||
| * struct in one source may reference a user-defined value type defined in | ||
| * another, so this must run across every AST in the build — not just the ones | ||
| * matched by the user's include globs. | ||
| */ | ||
| export function buildUserDefinedValueTypeIndex( | ||
| asts: unknown[], | ||
| ): UserDefinedValueTypeIndex { | ||
| const index: UserDefinedValueTypeIndex = new Map(); | ||
|
|
||
| for (const ast of asts) { | ||
| if (!isObject(ast) || ast.nodeType !== "SourceUnit") { | ||
| continue; | ||
| } | ||
|
|
||
| const topLevelNodes: unknown[] = Array.isArray(ast.nodes) ? ast.nodes : []; | ||
| for (const node of topLevelNodes) { | ||
| if (!isObject(node)) { | ||
| continue; | ||
| } | ||
|
|
||
| if (node.nodeType === "UserDefinedValueTypeDefinition") { | ||
| recordUserDefinedValueType(node, index); | ||
| } else if (node.nodeType === "ContractDefinition") { | ||
| const members: unknown[] = Array.isArray(node.nodes) ? node.nodes : []; | ||
| for (const member of members) { | ||
| if ( | ||
| isObject(member) && | ||
| member.nodeType === "UserDefinedValueTypeDefinition" | ||
| ) { | ||
| recordUserDefinedValueType(member, index); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return index; | ||
| } | ||
|
|
||
| function recordUserDefinedValueType( | ||
| node: Record<string, unknown>, | ||
| index: UserDefinedValueTypeIndex, | ||
| ): void { | ||
| if ( | ||
| typeof node.id === "number" && | ||
| isObject(node.underlyingType) && | ||
| node.underlyingType.nodeType === "ElementaryTypeName" | ||
| ) { | ||
| index.set(node.id, node.underlyingType); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns every struct definition reachable from a solc source AST (Abstract Syntax Tree), | ||
| * including structs nested inside contracts. | ||
| */ | ||
| export function extractStructsFromAst( | ||
| ast: unknown, | ||
| sourcePath: string, | ||
| userDefinedValueTypeI: UserDefinedValueTypeIndex = new Map(), | ||
| ): CollectedStruct[] { | ||
| if (!isObject(ast) || ast.nodeType !== "SourceUnit") { | ||
| return []; | ||
| } | ||
|
|
||
| const results: CollectedStruct[] = []; | ||
| const topLevelNodes: unknown[] = Array.isArray(ast.nodes) ? ast.nodes : []; | ||
|
|
||
| for (const node of topLevelNodes) { | ||
| if (!isObject(node)) { | ||
| continue; | ||
| } | ||
|
|
||
| if (node.nodeType === "StructDefinition") { | ||
| const collected = collectStruct(node, sourcePath, userDefinedValueTypeI); | ||
| if (collected !== undefined) { | ||
| results.push(collected); | ||
| } | ||
| } else if (node.nodeType === "ContractDefinition") { | ||
| const members: unknown[] = Array.isArray(node.nodes) ? node.nodes : []; | ||
| for (const member of members) { | ||
| if (isObject(member) && member.nodeType === "StructDefinition") { | ||
| const collected = collectStruct( | ||
| member, | ||
| sourcePath, | ||
| userDefinedValueTypeI, | ||
| ); | ||
| if (collected !== undefined) { | ||
| results.push(collected); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return results; | ||
| } | ||
|
|
||
| function collectStruct( | ||
| node: Record<string, unknown>, | ||
| sourcePath: string, | ||
| userDefinedValueTypeI: UserDefinedValueTypeIndex, | ||
| ): CollectedStruct | undefined { | ||
| if (typeof node.name !== "string") { | ||
| return undefined; | ||
| } | ||
|
|
||
| const memberNodes: unknown[] = Array.isArray(node.members) | ||
| ? node.members | ||
| : []; | ||
| const members: StructMember[] = []; | ||
|
|
||
| for (const memberNode of memberNodes) { | ||
| if ( | ||
| !isObject(memberNode) || | ||
| memberNode.nodeType !== "VariableDeclaration" | ||
| ) { | ||
| continue; | ||
| } | ||
|
|
||
| if (typeof memberNode.name !== "string") { | ||
| continue; | ||
| } | ||
|
|
||
| members.push({ | ||
| name: memberNode.name, | ||
| type: encodeMemberType(memberNode.typeName, userDefinedValueTypeI), | ||
| }); | ||
| } | ||
|
|
||
| return { | ||
| name: node.name, | ||
| members, | ||
| sourcePath, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Converts a solc `typeName` AST node into its EIP-712 type string, following | ||
| * the same conventions as `forge bind-json`: | ||
| * | ||
| * - elementary types pass through (`address`, `uint256`, `string`, ...) | ||
| * - enums → `uint8` | ||
| * - contracts / interfaces → `address` | ||
| * - structs → bare name (`Wallet.Person` → `Person`) | ||
| * - user-defined value types → underlying elementary type (`type Foo is bytes32` → `bytes32`) | ||
| * - arrays → `T[]` (dynamic) or `T[N]` (fixed) | ||
| * - mappings / functions → `undefined` (not EIP-712 encodable) | ||
| */ | ||
| export function encodeMemberType( | ||
| typeName: unknown, | ||
| userDefinedValueTypeI: UserDefinedValueTypeIndex = new Map(), | ||
| ): string | undefined { | ||
| if (!isObject(typeName)) { | ||
| return undefined; | ||
| } | ||
|
|
||
| switch (typeName.nodeType) { | ||
| case "ElementaryTypeName": { | ||
| // Prefer `typeDescriptions.typeString` over `name`: solc emits the | ||
| // unresolved alias in `name` (`uint`, `int`, `byte`), but the canonical | ||
| // EIP-712 type is in `typeString` (`uint256`, `int256`, `bytes1`). | ||
| // `address payable` is the exception — `typeString` is `"address payable"` | ||
| // while the canonical EIP-712 type is just `address`. | ||
| const desc = isObject(typeName.typeDescriptions) | ||
| ? typeName.typeDescriptions | ||
| : undefined; | ||
| const typeString = | ||
| typeof desc?.typeString === "string" ? desc.typeString : undefined; | ||
| if (typeString !== undefined) { | ||
| return typeString.endsWith(" payable") | ||
| ? typeString.slice(0, -" payable".length) | ||
| : typeString; | ||
| } | ||
|
|
||
| return typeof typeName.name === "string" ? typeName.name : undefined; | ||
| } | ||
|
schaable marked this conversation as resolved.
|
||
|
|
||
| case "UserDefinedTypeName": { | ||
| // `typeDescriptions.typeString` is the only reliable way to tell what | ||
| // kind of user-defined type this is — e.g. "struct Foo", "enum Bar", | ||
| // "contract Token". The AST node itself doesn't say. Note that solc | ||
| // emits the "contract " prefix for interface references as well. | ||
| const desc = isObject(typeName.typeDescriptions) | ||
| ? typeName.typeDescriptions | ||
| : undefined; | ||
| const typeString = | ||
| typeof desc?.typeString === "string" ? desc.typeString : ""; | ||
|
|
||
| if (typeString.startsWith("enum ")) { | ||
| return "uint8"; | ||
| } | ||
|
|
||
| if (typeString.startsWith("contract ")) { | ||
| return "address"; | ||
| } | ||
|
|
||
| if (typeString.startsWith("struct ")) { | ||
| // EIP-712 references structs by their bare name, so strip both the | ||
| // "struct " prefix, any storage-location suffix solc may append | ||
| // ("memory", "storage", ...), and any qualifier ("Wallet.Person"). | ||
| const remainder = typeString.slice("struct ".length).trim(); | ||
| const namePart = remainder.split(/\s+/)[0]; | ||
| const segments = namePart.split("."); | ||
| return segments[segments.length - 1]; | ||
| } | ||
|
|
||
| // User-defined value types (`type Foo is bytes32;`, solc 0.8.8+). | ||
| // Resolve via `referencedDeclaration` against the build-wide | ||
| // user-defined value type index | ||
| // and recurse on the underlying elementary type — matching forge's | ||
| // `Resolver::resolve_type` so `Foo h` encodes as `bytes32 h`, not `Foo h`. | ||
| const refId = | ||
| typeof typeName.referencedDeclaration === "number" | ||
| ? typeName.referencedDeclaration | ||
| : isObject(typeName.pathNode) && | ||
| typeof typeName.pathNode.referencedDeclaration === "number" | ||
| ? typeName.pathNode.referencedDeclaration | ||
| : undefined; | ||
|
|
||
| if (refId !== undefined) { | ||
| const underlying = userDefinedValueTypeI.get(refId); | ||
| if (underlying !== undefined) { | ||
| return encodeMemberType(underlying, userDefinedValueTypeI); | ||
| } | ||
| } | ||
|
|
||
| // Fallback when the reference can't be resolved (missing | ||
| // `referencedDeclaration`, or its definition wasn't in the build). | ||
| // Emitting the name lets the downstream encoder produce a clear error | ||
| // rather than failing silently here. | ||
| if (typeof typeName.name === "string") { | ||
| return typeName.name; | ||
| } | ||
|
|
||
| if ( | ||
| isObject(typeName.pathNode) && | ||
| typeof typeName.pathNode.name === "string" | ||
| ) { | ||
| const segments = typeName.pathNode.name.split("."); | ||
| return segments[segments.length - 1]; | ||
| } | ||
|
|
||
| return undefined; | ||
| } | ||
|
|
||
| case "ArrayTypeName": { | ||
| const base = encodeMemberType(typeName.baseType, userDefinedValueTypeI); | ||
| if (base === undefined) { | ||
| return undefined; | ||
| } | ||
|
|
||
| // solc omits `length` entirely for dynamic arrays; it isn't emitted as `null`. | ||
| const length = typeName.length; | ||
| if (length === null || length === undefined) { | ||
| return `${base}[]`; | ||
| } | ||
|
|
||
| // Always read from typeString: `Literal.value` preserves source text | ||
| // (`0x100`, `1_000`) but typeString canonicalizes to decimal. | ||
| const desc = isObject(typeName.typeDescriptions) | ||
| ? typeName.typeDescriptions | ||
| : undefined; | ||
| const typeString = | ||
| typeof desc?.typeString === "string" ? desc.typeString : ""; | ||
| const match = /\[(\d+)\]$/.exec(typeString); | ||
|
|
||
| if (match !== null) { | ||
| return `${base}[${match[1]}]`; | ||
| } | ||
|
|
||
| return undefined; | ||
| } | ||
|
|
||
| case "Mapping": | ||
| return undefined; | ||
|
|
||
| case "FunctionTypeName": | ||
| // EIP-712 can't encode function types. | ||
| return undefined; | ||
|
|
||
| default: | ||
| return undefined; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.