From 4cfe2b20410d12c8e22c1047e5ebf692cc3ca7c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petar=20Penovi=C4=87?= Date: Wed, 4 Dec 2024 23:21:34 +0100 Subject: [PATCH 1/3] fix: correct enum typed data hashing (#1281) --- __tests__/utils/typedData.test.ts | 2 +- src/utils/typedData.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/__tests__/utils/typedData.test.ts b/__tests__/utils/typedData.test.ts index 945f12075..ccbdc3dc1 100644 --- a/__tests__/utils/typedData.test.ts +++ b/__tests__/utils/typedData.test.ts @@ -326,7 +326,7 @@ describe('typedData', () => { messageHash = getMessageHash(exampleEnum, exampleAddress); expect(messageHash).toMatchInlineSnapshot( - `"0x6e61abaf480b1370bbf231f54e298c5f4872f40a6d2dd409ff30accee5bbd1e"` + `"0x416b85b18063b1b3420ab709e9d5e35cb716691d397c5841ce7c5198ee30bf"` ); expect(spyPedersen).not.toHaveBeenCalled(); diff --git a/src/utils/typedData.ts b/src/utils/typedData.ts index 1d462283a..fc9407c6c 100644 --- a/src/utils/typedData.ts +++ b/src/utils/typedData.ts @@ -357,11 +357,13 @@ export function encodeValue( if (revision === Revision.ACTIVE) { const [variantKey, variantData] = Object.entries(data as TypedData['message'])[0]; - const parentType = types[ctx.parent as string].find((t) => t.name === ctx.key); - const enumType = types[(parentType as StarknetEnumType).contains]; + const parentType = types[ctx.parent as string].find((t) => t.name === ctx.key)!; + const enumName = (parentType as StarknetEnumType).contains; + const enumType = types[enumName]; const variantType = enumType.find((t) => t.name === variantKey) as StarknetType; const variantIndex = enumType.indexOf(variantType); + const typeHash = getTypeHash(types, enumName, revision); const encodedSubtypes = variantType.type .slice(1, -1) .split(',') @@ -372,7 +374,7 @@ export function encodeValue( }); return [ type, - revisionConfiguration[revision].hashMethod([variantIndex, ...encodedSubtypes]), + revisionConfiguration[revision].hashMethod([typeHash, variantIndex, ...encodedSubtypes]), ]; } // else fall through to default return [type, getHex(data as string)]; From 556dfd778367f8a5e41a676c75227d37727f71fb Mon Sep 17 00:00:00 2001 From: Jonathan LEI Date: Thu, 2 Jan 2025 20:06:55 +0800 Subject: [PATCH 2/3] fix: rectify snip-12 violation by removing extra `:` in enum encoding (#1288) --- __tests__/utils/typedData.test.ts | 6 +++--- src/utils/typedData.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/__tests__/utils/typedData.test.ts b/__tests__/utils/typedData.test.ts index ccbdc3dc1..891e1aa3a 100644 --- a/__tests__/utils/typedData.test.ts +++ b/__tests__/utils/typedData.test.ts @@ -64,7 +64,7 @@ describe('typedData', () => { ); encoded = encodeType(exampleEnum.types, 'Example', TypedDataRevision.ACTIVE); expect(encoded).toMatchInlineSnapshot( - `"\\"Example\\"(\\"someEnum1\\":\\"EnumA\\",\\"someEnum2\\":\\"EnumB\\")\\"EnumA\\"(\\"Variant 1\\":(),\\"Variant 2\\":(\\"u128\\",\\"u128*\\"),\\"Variant 3\\":(\\"u128\\"))\\"EnumB\\"(\\"Variant 1\\":(),\\"Variant 2\\":(\\"u128\\"))"` + `"\\"Example\\"(\\"someEnum1\\":\\"EnumA\\",\\"someEnum2\\":\\"EnumB\\")\\"EnumA\\"(\\"Variant 1\\"(),\\"Variant 2\\"(\\"u128\\",\\"u128*\\"),\\"Variant 3\\"(\\"u128\\"))\\"EnumB\\"(\\"Variant 1\\"(),\\"Variant 2\\"(\\"u128\\"))"` ); }); @@ -104,7 +104,7 @@ describe('typedData', () => { ); typeHash = getTypeHash(exampleEnum.types, 'Example', TypedDataRevision.ACTIVE); expect(typeHash).toMatchInlineSnapshot( - `"0x8eb4aeac64b707f3e843284c4258df6df1f0f7fd38dcffdd8a153a495cd351"` + `"0x393bf83422ca8626a2932696cfa0acb19dcad6de2fe84a2dd2ca7607ea5329a"` ); }); @@ -326,7 +326,7 @@ describe('typedData', () => { messageHash = getMessageHash(exampleEnum, exampleAddress); expect(messageHash).toMatchInlineSnapshot( - `"0x416b85b18063b1b3420ab709e9d5e35cb716691d397c5841ce7c5198ee30bf"` + `"0x150a589bb56a4fbf4ee01f52e44fd5adde6af94c02b37e383413fed185321a2"` ); expect(spyPedersen).not.toHaveBeenCalled(); diff --git a/src/utils/typedData.ts b/src/utils/typedData.ts index fc9407c6c..6a579dbd3 100644 --- a/src/utils/typedData.ts +++ b/src/utils/typedData.ts @@ -266,8 +266,8 @@ export function encodeType( .split(',') .map((e) => (e ? esc(e) : e)) .join(',')})` - : esc(targetType); - return `${esc(t.name)}:${typeString}`; + : `:${esc(targetType)}`; + return `${esc(t.name)}${typeString}`; }); return `${esc(dependency)}(${dependencyElements})`; }) From 0e0a3eb17f6f2fbd09170619290167b10dab9218 Mon Sep 17 00:00:00 2001 From: Jonathan LEI Date: Tue, 7 Jan 2025 21:02:54 +0800 Subject: [PATCH 3/3] fix: repair snip-12 enum type nested dependency (#1289) * fix: missing snip-12 enum type dependency * test: add nested enum tests --------- Co-authored-by: Petar Penovic --- __mocks__/typedData/example_enumNested.json | 33 ++++++++++++++++ __tests__/utils/typedData.test.ts | 14 +++++++ src/utils/typedData.ts | 44 +++++++++++++-------- 3 files changed, 74 insertions(+), 17 deletions(-) create mode 100644 __mocks__/typedData/example_enumNested.json diff --git a/__mocks__/typedData/example_enumNested.json b/__mocks__/typedData/example_enumNested.json new file mode 100644 index 000000000..6ee6a75bd --- /dev/null +++ b/__mocks__/typedData/example_enumNested.json @@ -0,0 +1,33 @@ +{ + "types": { + "StarknetDomain": [ + { "name": "name", "type": "shortstring" }, + { "name": "version", "type": "shortstring" }, + { "name": "chainId", "type": "shortstring" }, + { "name": "revision", "type": "shortstring" } + ], + "Example": [{ "name": "someEnum", "type": "enum", "contains": "EnumA" }], + "EnumA": [ + { "name": "Variant 1", "type": "()" }, + { "name": "Variant 2", "type": "(u128,StructA)" } + ], + "StructA": [{ "name": "nestedEnum", "type": "enum", "contains": "EnumB" }], + "EnumB": [ + { "name": "Variant A", "type": "()" }, + { "name": "Variant B", "type": "(StructB*)" } + ], + "StructB": [{ "name": "flag", "type": "bool" }] + }, + "primaryType": "Example", + "domain": { + "name": "StarkNet Mail", + "version": "1", + "chainId": "1", + "revision": "1" + }, + "message": { + "someEnum": { + "Variant 2": [2, { "nestedEnum": { "Variant B": [[{ "flag": true }, { "flag": false }]] } }] + } + } +} diff --git a/__tests__/utils/typedData.test.ts b/__tests__/utils/typedData.test.ts index 891e1aa3a..c21ea251d 100644 --- a/__tests__/utils/typedData.test.ts +++ b/__tests__/utils/typedData.test.ts @@ -3,6 +3,7 @@ import * as starkCurve from '@scure/starknet'; import typedDataExample from '../../__mocks__/typedData/baseExample.json'; import exampleBaseTypes from '../../__mocks__/typedData/example_baseTypes.json'; import exampleEnum from '../../__mocks__/typedData/example_enum.json'; +import exampleEnumNested from '../../__mocks__/typedData/example_enumNested.json'; import examplePresetTypes from '../../__mocks__/typedData/example_presetTypes.json'; import typedDataStructArrayExample from '../../__mocks__/typedData/mail_StructArray.json'; import typedDataSessionExample from '../../__mocks__/typedData/session_MerkleTree.json'; @@ -66,6 +67,10 @@ describe('typedData', () => { expect(encoded).toMatchInlineSnapshot( `"\\"Example\\"(\\"someEnum1\\":\\"EnumA\\",\\"someEnum2\\":\\"EnumB\\")\\"EnumA\\"(\\"Variant 1\\"(),\\"Variant 2\\"(\\"u128\\",\\"u128*\\"),\\"Variant 3\\"(\\"u128\\"))\\"EnumB\\"(\\"Variant 1\\"(),\\"Variant 2\\"(\\"u128\\"))"` ); + encoded = encodeType(exampleEnumNested.types, 'Example', TypedDataRevision.ACTIVE); + expect(encoded).toMatchInlineSnapshot( + `"\\"Example\\"(\\"someEnum\\":\\"EnumA\\")\\"EnumA\\"(\\"Variant 1\\"(),\\"Variant 2\\"(\\"u128\\",\\"StructA\\"))\\"EnumB\\"(\\"Variant A\\"(),\\"Variant B\\"(\\"StructB*\\"))\\"StructA\\"(\\"nestedEnum\\":\\"EnumB\\")\\"StructB\\"(\\"flag\\":\\"bool\\")"` + ); }); test('should get right type hash', () => { @@ -106,6 +111,10 @@ describe('typedData', () => { expect(typeHash).toMatchInlineSnapshot( `"0x393bf83422ca8626a2932696cfa0acb19dcad6de2fe84a2dd2ca7607ea5329a"` ); + typeHash = getTypeHash(exampleEnumNested.types, 'Example', TypedDataRevision.ACTIVE); + expect(typeHash).toMatchInlineSnapshot( + `"0x267f739fd83d30528a0fafb23df33b6c35ca0a5adbcfb32152721478fa9d0ce"` + ); }); test('should transform type selector', () => { @@ -329,6 +338,11 @@ describe('typedData', () => { `"0x150a589bb56a4fbf4ee01f52e44fd5adde6af94c02b37e383413fed185321a2"` ); + messageHash = getMessageHash(exampleEnumNested, exampleAddress); + expect(messageHash).toMatchInlineSnapshot( + `"0x6e70eb4ef625dda451094716eee7f31fa81ca0ba99d390885e9c7b0d64cd22"` + ); + expect(spyPedersen).not.toHaveBeenCalled(); expect(spyPoseidon).toHaveBeenCalled(); spyPedersen.mockRestore(); diff --git a/src/utils/typedData.ts b/src/utils/typedData.ts index 6a579dbd3..d16f34257 100644 --- a/src/utils/typedData.ts +++ b/src/utils/typedData.ts @@ -167,36 +167,46 @@ export function getDependencies( contains: string = '', revision: Revision = Revision.LEGACY ): string[] { + let dependencyTypes: string[] = [type]; + // Include pointers (struct arrays) if (type[type.length - 1] === '*') { - type = type.slice(0, -1); + dependencyTypes = [type.slice(0, -1)]; } else if (revision === Revision.ACTIVE) { // enum base if (type === 'enum') { - type = contains; + dependencyTypes = [contains]; } // enum element types else if (type.match(/^\(.*\)$/)) { - type = type.slice(1, -1); + dependencyTypes = type + .slice(1, -1) + .split(',') + .map((depType) => (depType[depType.length - 1] === '*' ? depType.slice(0, -1) : depType)); } } - if (dependencies.includes(type) || !types[type]) { - return dependencies; - } - - return [ - type, - ...(types[type] as StarknetEnumType[]).reduce( - (previous, t) => [ - ...previous, - ...getDependencies(types, t.type, previous, t.contains, revision).filter( - (dependency) => !previous.includes(dependency) - ), + return dependencyTypes + .filter((t) => !dependencies.includes(t) && types[t]) + .reduce( + // This comment prevents prettier from rolling everything here into a single line. + (p, depType) => [ + ...p, + ...[ + depType, + ...(types[depType] as StarknetEnumType[]).reduce( + (previous, t) => [ + ...previous, + ...getDependencies(types, t.type, previous, t.contains, revision).filter( + (dependency) => !previous.includes(dependency) + ), + ], + [] + ), + ].filter((dependency) => !p.includes(dependency)), ], [] - ), - ]; + ); } function getMerkleTreeType(types: TypedData['types'], ctx: Context) {