From 8f99601df1f26a8ba4d6d9dea5e033e7f688107e Mon Sep 17 00:00:00 2001 From: Richard Moore Date: Tue, 16 Jan 2024 17:41:57 -0500 Subject: [PATCH] Fixed normalization and abstracted EIP-712 Array parsing (#4541). --- src.ts/_tests/test-hash-typeddata.ts | 60 ++++++++++++++++++++++++++++ src.ts/hash/typed-data.ts | 58 +++++++++++++++++++++------ 2 files changed, 105 insertions(+), 13 deletions(-) diff --git a/src.ts/_tests/test-hash-typeddata.ts b/src.ts/_tests/test-hash-typeddata.ts index 57eec5b87..5b0a82744 100644 --- a/src.ts/_tests/test-hash-typeddata.ts +++ b/src.ts/_tests/test-hash-typeddata.ts @@ -68,6 +68,66 @@ describe("Tests Typed Data (EIP-712) aliases", function() { }, encoded: "0xa272ada5f88085e4cb18acdb87bd057a8cbfec249fee53de0149409080947cf500000000000000000000000000000000000000000000000000000000000000231c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8" }, + { + name: "array-uint", + types: { + foo: [ + { name: "a", type: "uint256[]" }, + { name: "b", type: "string" }, + ], + }, + typesAlias: { + foo: [ + { name: "a", type: "uint[]" }, + { name: "b", type: "string" }, + ], + }, + data: { + a: [ 35, 36, 37 ], + b: "hello" + }, + encoded: "0x1a961843d0002bdd66ec21afd6e4a5b0aac34a4b6112890378c6e3a38b752e0b0c22b846886e98aeffc1f1166d4b35868da4d4da853dcb3b2856cfc233fd10c81c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8" + }, + { + name: "array-int", + types: { + foo: [ + { name: "a", type: "int256[]" }, + { name: "b", type: "string" }, + ], + }, + typesAlias: { + foo: [ + { name: "a", type: "int[]" }, + { name: "b", type: "string" }, + ], + }, + data: { + a: [ 35, 36, 37 ], + b: "hello" + }, + encoded: "0x0b89085a01a3b67d2231c6a136f9c8eea75d7d479a83a127356f8540ee15af010c22b846886e98aeffc1f1166d4b35868da4d4da853dcb3b2856cfc233fd10c81c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8" + }, + { + name: "nested-array-uint", + types: { + foo: [ + { name: "a", type: "uint256[][]" }, + { name: "b", type: "string" }, + ], + }, + typesAlias: { + foo: [ + { name: "a", type: "uint[][]" }, + { name: "b", type: "string" }, + ], + }, + data: { + a: [ [ 35, 36 ], [ 37 ] ], + b: "hello" + }, + encoded: "0x5efa7c4b66979cf78fcc7c3e71cbfa04ec2c7529002642082bf20a91552c1147fa5ffe3a0504d850bc7c9eeda1cf960b596b73f4dc0272a6fa89dace08e320291c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8" + }, ]; for (const test of tests) { diff --git a/src.ts/hash/typed-data.ts b/src.ts/hash/typed-data.ts index e1e50ca92..5a53ab4ec 100644 --- a/src.ts/hash/typed-data.ts +++ b/src.ts/hash/typed-data.ts @@ -181,6 +181,35 @@ function encodeType(name: string, fields: Array): string { return `${ name }(${ fields.map(({ name, type }) => (type + " " + name)).join(",") })`; } +type ArrayResult = { + base: string; // The base type + index?: string; // the full Index (if any) + array?: { // The Array... (if index) + base: string; // ...base type (same as above) + prefix: string; // ...sans the final Index + count: number; // ...the final Index (-1 for dynamic) + } +}; + +// foo[][3] => { base: "foo", index: "[][3]", array: { +// base: "foo", prefix: "foo[]", count: 3 } } +function splitArray(type: string): ArrayResult { + const match = type.match(/^([^\x5b]*)((\x5b\d*\x5d)*)(\x5b(\d*)\x5d)$/); + if (match) { + return { + base: match[1], + index: (match[2] + match[4]), + array: { + base: match[1], + prefix: (match[1] + match[2]), + count: (match[5] ? parseInt(match[5]): -1), + } + }; + } + + return { base: type }; +} + /** * A **TypedDataEncode** prepares and encodes [[link-eip-712]] payloads * for signed typed data. @@ -235,11 +264,14 @@ export class TypedDataEncoder { const types: Record> = { }; Object.keys(_types).forEach((type) => { - // Normalize int/uint unless they are a complex type themselves types[type] = _types[type].map(({ name, type }) => { - if (type === "int" && !_types["int"]) { type = "int256"; } - if (type === "uint" && !_types["uint"]) { type = "uint256"; } - return { name, type }; + + // Normalize the base type (unless name conflict) + let { base, index } = splitArray(type); + if (base === "int" && !_types["int"]) { base = "int256"; } + if (base === "uint" && !_types["uint"]) { base = "uint256"; } + + return { name, type: (base + (index || "")) }; }); links.set(type, new Set()); @@ -258,7 +290,7 @@ export class TypedDataEncoder { uniqueNames.add(field.name); // Get the base type (drop any array specifiers) - const baseType = ((field.type.match(/^([^\x5b]*)(\x5b|$)/)))[1] || null; + const baseType = splitArray(field.type).base; assertArgument(baseType !== name, `circular type reference to ${ JSON.stringify(baseType) }`, "types", _types); // Is this a base encoding type? @@ -331,12 +363,12 @@ export class TypedDataEncoder { } // Array - const match = type.match(/^(.*)(\x5b(\d*)\x5d)$/); - if (match) { - const subtype = match[1]; + const array = splitArray(type).array; + if (array) { + const subtype = array.prefix; const subEncoder = this.getEncoder(subtype); return (value: Array) => { - assertArgument(!match[3] || parseInt(match[3]) === value.length, `array length mismatch; expected length ${ parseInt(match[3]) }`, "value", value); + assertArgument(array.count === -1 || array.count === value.length, `array length mismatch; expected length ${ array.count }`, "value", value); let result = value.map(subEncoder); if (this.#fullTypes.has(subtype)) { @@ -413,10 +445,10 @@ export class TypedDataEncoder { } // Array - const match = type.match(/^(.*)(\x5b(\d*)\x5d)$/); - if (match) { - assertArgument(!match[3] || parseInt(match[3]) === value.length, `array length mismatch; expected length ${ parseInt(match[3]) }`, "value", value); - return value.map((v: any) => this._visit(match[1], v, callback)); + const array = splitArray(type).array; + if (array) { + assertArgument(array.count === -1 || array.count === value.length, `array length mismatch; expected length ${ array.count }`, "value", value); + return value.map((v: any) => this._visit(array.prefix, v, callback)); } // Struct