Skip to content

Commit 75e66b1

Browse files
fix: signTypedData verification for smart accounts (#2906)
1 parent d2541e5 commit 75e66b1

File tree

10 files changed

+309
-53
lines changed

10 files changed

+309
-53
lines changed

.changeset/afraid-pianos-cross.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Fix signTypedData external verification for smart accounts

legacy_packages/auth/test/smart-wallet.test.ts

Lines changed: 66 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ require("dotenv-mono").load();
99

1010
describe.runIf(process.env.TW_SECRET_KEY)(
1111
"Wallet Authentication - EVM - Smart Wallet",
12-
{
13-
timeout: 240000,
14-
},
1512
async () => {
1613
let adminWallet: any, signerWallet: any, attackerWallet: any;
1714
let auth: ThirdwebAuth;
@@ -42,7 +39,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
4239
auth.updateWallet(signerWallet);
4340
});
4441

45-
it("Should verify logged in wallet", async () => {
42+
it("Should verify logged in wallet", {
43+
timeout: 240000,
44+
}, async () => {
4645
const payload = await auth.login();
4746

4847
auth.updateWallet(adminWallet);
@@ -51,7 +50,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
5150
expect(address).to.equal(await signerWallet.getAddress());
5251
});
5352

54-
it("Should verify logged in wallet with chain ID and expiration", async () => {
53+
it("Should verify logged in wallet with chain ID and expiration", {
54+
timeout: 240000,
55+
}, async () => {
5556
const payload = await auth.login({
5657
expirationTime: new Date(Date.now() + 1000 * 60 * 5),
5758
chainId: "84532",
@@ -65,7 +66,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
6566
expect(address).to.equal(await signerWallet.getAddress());
6667
});
6768

68-
it("Should verify payload with resources", async () => {
69+
it("Should verify payload with resources", {
70+
timeout: 240000,
71+
}, async () => {
6972
const payload = await auth.login({
7073
resources: ["https://example.com", "https://test.com"],
7174
});
@@ -78,7 +81,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
7881
expect(address).to.equal(await signerWallet.getAddress());
7982
});
8083

81-
it("Should reject payload without necessary resources", async () => {
84+
it("Should reject payload without necessary resources", {
85+
timeout: 240000,
86+
}, async () => {
8287
const payload = await auth.login({
8388
resources: ["https://example.com"],
8489
});
@@ -96,7 +101,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
96101
}
97102
});
98103

99-
it("Should verify payload with customized statement", async () => {
104+
it("Should verify payload with customized statement", {
105+
timeout: 240000,
106+
}, async () => {
100107
const payload = await auth.login({
101108
statement: "Please sign!",
102109
});
@@ -109,7 +116,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
109116
expect(address).to.equal(await signerWallet.getAddress());
110117
});
111118

112-
it("Should reject payload with incorrect statement", async () => {
119+
it("Should reject payload with incorrect statement", {
120+
timeout: 240000,
121+
}, async () => {
113122
const payload = await auth.login({
114123
statement: "Please sign!",
115124
});
@@ -127,7 +136,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
127136
}
128137
});
129138

130-
it("Should reject invalid nonce", async () => {
139+
it("Should reject invalid nonce", {
140+
timeout: 240000,
141+
}, async () => {
131142
const payload = await auth.login();
132143

133144
auth.updateWallet(adminWallet);
@@ -145,7 +156,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
145156
}
146157
});
147158

148-
it("Should accept valid nonce", async () => {
159+
it("Should accept valid nonce", {
160+
timeout: 240000,
161+
}, async () => {
149162
const payload = await auth.login();
150163

151164
auth.updateWallet(adminWallet);
@@ -160,7 +173,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
160173
expect(address).to.equal(await signerWallet.getAddress());
161174
});
162175

163-
it("Should reject payload with incorrect domain", async () => {
176+
it("Should reject payload with incorrect domain", {
177+
timeout: 240000,
178+
}, async () => {
164179
const payload = await auth.login();
165180

166181
auth.updateWallet(adminWallet);
@@ -174,7 +189,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
174189
}
175190
});
176191

177-
it("Should reject expired login payload", async () => {
192+
it("Should reject expired login payload", {
193+
timeout: 240000,
194+
}, async () => {
178195
const payload = await auth.login({
179196
expirationTime: new Date(Date.now() - 1000 * 60 * 5),
180197
});
@@ -188,7 +205,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
188205
}
189206
});
190207

191-
it("Should reject payload with incorrect chain ID", async () => {
208+
it("Should reject payload with incorrect chain ID", {
209+
timeout: 240000,
210+
}, async () => {
192211
const payload = await auth.login({
193212
chainId: "1",
194213
});
@@ -206,7 +225,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
206225
}
207226
});
208227

209-
it("Should reject payload with incorrect signer", async () => {
228+
it("Should reject payload with incorrect signer", {
229+
timeout: 240000,
230+
}, async () => {
210231
const payload = await auth.login();
211232
payload.payload.address = await attackerWallet.getAddress();
212233

@@ -219,7 +240,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
219240
}
220241
});
221242

222-
it("Should generate valid authentication token", async () => {
243+
it("Should generate valid authentication token", {
244+
timeout: 240000,
245+
}, async () => {
223246
const payload = await auth.login();
224247

225248
auth.updateWallet(adminWallet);
@@ -229,7 +252,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
229252
expect(user.address).to.equal(await signerWallet.getAddress());
230253
});
231254

232-
it("Should reject token with incorrect domain", async () => {
255+
it("Should reject token with incorrect domain", {
256+
timeout: 240000,
257+
}, async () => {
233258
const payload = await auth.login();
234259

235260
auth.updateWallet(adminWallet);
@@ -245,7 +270,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
245270
}
246271
});
247272

248-
it("Should reject token before invalid before", async () => {
273+
it("Should reject token before invalid before", {
274+
timeout: 240000,
275+
}, async () => {
249276
const payload = await auth.login();
250277

251278
auth.updateWallet(adminWallet);
@@ -261,7 +288,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
261288
}
262289
});
263290

264-
it("Should reject expired authentication token", async () => {
291+
it("Should reject expired authentication token", {
292+
timeout: 240000,
293+
}, async () => {
265294
const payload = await auth.login();
266295

267296
auth.updateWallet(adminWallet);
@@ -277,7 +306,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
277306
}
278307
});
279308

280-
it("Should reject if admin address is not connected wallet address", async () => {
309+
it("Should reject if admin address is not connected wallet address", {
310+
timeout: 240000,
311+
}, async () => {
281312
const payload = await auth.login();
282313

283314
auth.updateWallet(adminWallet);
@@ -294,7 +325,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
294325
}
295326
});
296327

297-
it("Should accept token with valid token ID", async () => {
328+
it("Should accept token with valid token ID", {
329+
timeout: 240000,
330+
}, async () => {
298331
const payload = await auth.login();
299332

300333
auth.updateWallet(adminWallet);
@@ -313,7 +346,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
313346
expect(user.address).to.equal(await signerWallet.getAddress());
314347
});
315348

316-
it("Should reject token with invalid token ID", async () => {
349+
it("Should reject token with invalid token ID", {
350+
timeout: 240000,
351+
}, async () => {
317352
const payload = await auth.login();
318353

319354
auth.updateWallet(adminWallet);
@@ -335,7 +370,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
335370
}
336371
});
337372

338-
it("Should propagate session on token", async () => {
373+
it("Should propagate session on token", {
374+
timeout: 240000,
375+
}, async () => {
339376
const payload = await auth.login();
340377

341378
auth.updateWallet(adminWallet);
@@ -349,7 +386,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
349386
expect(user.session).to.deep.equal({ role: "admin" });
350387
});
351388

352-
it("Should call session callback function", async () => {
389+
it("Should call session callback function", {
390+
timeout: 240000,
391+
}, async () => {
353392
const payload = await auth.login();
354393

355394
auth.updateWallet(adminWallet);
@@ -368,7 +407,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
368407
});
369408
});
370409

371-
it("Should authenticate with issuer address", async () => {
410+
it("Should authenticate with issuer address", {
411+
timeout: 240000,
412+
}, async () => {
372413
const payload = await auth.login();
373414

374415
auth.updateWallet(adminWallet);

legacy_packages/wallets/src/evm/connectors/smart-wallet/lib/check-contract-wallet-signature.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export async function checkContractWalletSignature(
7474

7575
const provider = new providers.StaticJsonRpcProvider(
7676
{
77-
url: chainIdToThirdwebRpc(chainId),
77+
url: rpcUrl,
7878
skipFetchSetup: _skipFetchSetup,
7979
headers,
8080
},

legacy_packages/wallets/src/evm/connectors/smart-wallet/lib/erc4337-signer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ Code: ${errorCode}`;
227227
try {
228228
const provider = new providers.StaticJsonRpcProvider(
229229
{
230-
url: chainIdToThirdwebRpc(chainId, this.config.clientId),
230+
url: rpcUrl,
231231
headers,
232232
},
233233
chainId,

legacy_packages/wallets/src/evm/wallets/abstract.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { createErc20 } from "../utils/currency";
1717
// TODO improve this
1818
export function chainIdToThirdwebRpc(chainId: number, clientId?: string) {
1919
return `https://${chainId}.rpc.thirdweb.com${clientId ? `/${clientId}` : ""}${
20-
typeof globalThis !== "undefined" && "APP_BUNDLE_ID" in globalThis
20+
typeof globalThis !== "undefined" && "APP_BUNDLE_ID" in globalThis && !!(globalThis as any).APP_BUNDLE_ID
2121
? `?bundleId=${(globalThis as any).APP_BUNDLE_ID as string}`
2222
: ""
2323
}`;

legacy_packages/wallets/test/smart-wallet-integration.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ describeIf(!!SECRET_KEY)(
117117
});
118118

119119
it("can sign and verify 1271 old factory", {
120-
timeout: 120_000,
120+
timeout: 240_000,
121121
}, async () => {
122122
const message = "0x1234";
123123
const sig = await smartWallet.signMessage(message);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { type TypedData, type TypedDataDefinition, hashTypedData } from "viem";
2+
import type { ThirdwebContract } from "../../contract/contract.js";
3+
import { isHex } from "../../utils/encoding/hex.js";
4+
import { isValidSignature } from "./__generated__/isValidSignature/read/isValidSignature.js";
5+
6+
export type CheckContractWalletSignTypedDataOptions<
7+
typedData extends TypedData | Record<string, unknown>,
8+
primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
9+
> = {
10+
contract: ThirdwebContract;
11+
data: TypedDataDefinition<typedData, primaryType>;
12+
signature: string;
13+
};
14+
const MAGIC_VALUE = "0x1626ba7e";
15+
16+
/**
17+
* Checks if a contract wallet signature is valid.
18+
* @param options - The options for the checkContractWalletSignature function.
19+
* @param options.contract - The contract to check the signature against.
20+
* @param options.message - The message to check the signature against.
21+
* @param options.signature - The signature to check.
22+
* @extension ERC1271
23+
* @example
24+
* ```ts
25+
* import { checkContractWalletSignedTypedData } from "thirdweb/extensions/erc1271";
26+
* const isValid = await checkContractWalletSignedTypedData({
27+
* contract: myContract,
28+
* data: {
29+
* primaryType: "EIP712Domain",
30+
* domain: {
31+
* name: "Example",
32+
* version: "1",
33+
* chainId: 1,
34+
* verifyingContract: myContract.address,
35+
* },
36+
* });
37+
* ```
38+
* @returns A promise that resolves with a boolean indicating if the signature is valid.
39+
*/
40+
export async function checkContractWalletSignedTypedData<
41+
typedData extends TypedData | Record<string, unknown>,
42+
primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
43+
>(options: CheckContractWalletSignTypedDataOptions<typedData, primaryType>) {
44+
if (!isHex(options.signature)) {
45+
throw new Error("The signature must be a valid hex string.");
46+
}
47+
const result = await isValidSignature({
48+
contract: options.contract,
49+
hash: hashTypedData(options.data),
50+
signature: options.signature,
51+
});
52+
return result === MAGIC_VALUE;
53+
}

0 commit comments

Comments
 (0)