Skip to content

Commit d1716fc

Browse files
feat: Support ERC6492 smart account signatures (#5641)
1 parent ed0886a commit d1716fc

File tree

6 files changed

+194
-128
lines changed

6 files changed

+194
-128
lines changed

.changeset/strong-beans-pump.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+
Support ERC6492 for smart account signatures

packages/thirdweb/src/auth/verify-hash.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ export async function verifyHash({
129129
try {
130130
const result = await eth_call(rpcRequest, verificationData);
131131
return hexToBool(result);
132-
} catch {
132+
} catch (err) {
133+
console.error("Error verifying ERC-6492 signature", err);
133134
// Some chains do not support the eth_call simulation and will fail, so we fall back to regular EIP1271 validation
134135
const validEip1271 = await verifyEip1271Signature({
135136
hash,
@@ -153,7 +154,7 @@ export async function verifyHash({
153154
}
154155

155156
const EIP_1271_MAGIC_VALUE = "0x1626ba7e";
156-
export async function verifyEip1271Signature({
157+
async function verifyEip1271Signature({
157158
hash,
158159
signature,
159160
contract,

packages/thirdweb/src/wallets/smart/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,8 @@ async function createSmartAccount(
284284

285285
const { deployAndSignMessage } = await import("./lib/signing.js");
286286
return deployAndSignMessage({
287-
account,
288287
accountContract,
288+
factoryContract: options.factoryContract,
289289
options,
290290
message,
291291
});
@@ -305,8 +305,8 @@ async function createSmartAccount(
305305

306306
const { deployAndSignTypedData } = await import("./lib/signing.js");
307307
return deployAndSignTypedData({
308-
account,
309308
accountContract,
309+
factoryContract: options.factoryContract,
310310
options,
311311
typedData,
312312
});
Lines changed: 103 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,41 @@
1+
import type { Hex } from "ox";
12
import type {
23
SignableMessage,
34
TypedData,
45
TypedDataDefinition,
56
TypedDataDomain,
67
} from "viem";
8+
import { serializeErc6492Signature } from "../../../auth/serialize-erc6492-signature.js";
9+
import { verifyHash } from "../../../auth/verify-hash.js";
710
import {
8-
verifyEip1271Signature,
9-
verifyHash,
10-
} from "../../../auth/verify-hash.js";
11-
import type { ThirdwebContract } from "../../../contract/contract.js";
11+
type ThirdwebContract,
12+
getContract,
13+
} from "../../../contract/contract.js";
14+
import { encode } from "../../../transaction/actions/encode.js";
1215
import { readContract } from "../../../transaction/read-contract.js";
1316
import { encodeAbiParameters } from "../../../utils/abi/encodeAbiParameters.js";
14-
import { isContractDeployed } from "../../../utils/bytecode/is-contract-deployed.js";
1517
import { hashMessage } from "../../../utils/hashing/hashMessage.js";
1618
import { hashTypedData } from "../../../utils/hashing/hashTypedData.js";
17-
import type { Account } from "../../interfaces/wallet.js";
1819
import type { SmartAccountOptions } from "../types.js";
20+
import { prepareCreateAccount } from "./calls.js";
1921

2022
export async function deployAndSignMessage({
21-
account,
2223
accountContract,
24+
factoryContract,
2325
options,
2426
message,
2527
}: {
26-
account: Account;
2728
accountContract: ThirdwebContract;
29+
factoryContract: ThirdwebContract;
2830
options: SmartAccountOptions;
2931
message: SignableMessage;
3032
}) {
31-
const isDeployed = await isContractDeployed(accountContract);
32-
if (!isDeployed) {
33-
await _deployAccount({
34-
options,
35-
account,
36-
accountContract,
37-
});
38-
// the bundler and rpc might not be in sync, so while the bundler has a transaction hash for the deployment,
39-
// the rpc might not have it yet, so we wait until the rpc confirms the contract is deployed
40-
await confirmContractDeployment({
41-
accountContract,
42-
});
43-
}
44-
4533
const originalMsgHash = hashMessage(message);
46-
// check if the account contract supports EIP721 domain separator or modular based signing
47-
const is712Factory = await readContract({
48-
contract: accountContract,
49-
method:
50-
"function getMessageHash(bytes32 _hash) public view returns (bytes32)",
51-
params: [originalMsgHash],
52-
})
53-
.then((res) => res !== "0x")
54-
.catch(() => false);
34+
const is712Factory = await checkFor712Factory({
35+
factoryContract,
36+
accountContract,
37+
originalMsgHash,
38+
});
5539

5640
let sig: `0x${string}`;
5741
if (is712Factory) {
@@ -75,31 +59,50 @@ export async function deployAndSignMessage({
7559
sig = await options.personalAccount.signMessage({ message });
7660
}
7761

78-
const isValid = await verifyEip1271Signature({
79-
contract: accountContract,
80-
hash: originalMsgHash,
62+
const deployTx = prepareCreateAccount({
63+
factoryContract,
64+
adminAddress: options.personalAccount.address,
65+
accountSalt: options.overrides?.accountSalt,
66+
createAccountOverride: options.overrides?.createAccount,
67+
});
68+
if (!deployTx) {
69+
throw new Error("Create account override not provided");
70+
}
71+
const initCode = await encode(deployTx);
72+
const erc6492Sig = serializeErc6492Signature({
73+
address: factoryContract.address,
74+
data: initCode,
8175
signature: sig,
8276
});
8377

78+
// check if the signature is valid
79+
const isValid = await verifyHash({
80+
hash: originalMsgHash,
81+
signature: erc6492Sig,
82+
address: accountContract.address,
83+
chain: accountContract.chain,
84+
client: accountContract.client,
85+
});
86+
8487
if (isValid) {
85-
return sig;
88+
return erc6492Sig;
8689
}
8790
throw new Error(
88-
"Unable to verify signature on smart account, please make sure the smart account is deployed and the signature is valid.",
91+
"Unable to verify signature on smart account, please make sure the admin wallet has permissions and the signature is valid.",
8992
);
9093
}
9194

9295
export async function deployAndSignTypedData<
9396
const typedData extends TypedData | Record<string, unknown>,
9497
primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
9598
>({
96-
account,
9799
accountContract,
100+
factoryContract,
98101
options,
99102
typedData,
100103
}: {
101-
account: Account;
102104
accountContract: ThirdwebContract;
105+
factoryContract: ThirdwebContract;
103106
options: SmartAccountOptions;
104107
typedData: TypedDataDefinition<typedData, primaryType>;
105108
}) {
@@ -112,38 +115,16 @@ export async function deployAndSignTypedData<
112115
return options.personalAccount.signTypedData(typedData);
113116
}
114117

115-
const isDeployed = await isContractDeployed(accountContract);
116-
if (!isDeployed) {
117-
await _deployAccount({
118-
options,
119-
account,
120-
accountContract,
121-
});
122-
// the bundler and rpc might not be in sync, so while the bundler has a transaction hash for the deployment,
123-
// the rpc might not have it yet, so we wait until the rpc confirms the contract is deployed
124-
await confirmContractDeployment({
125-
accountContract,
126-
});
127-
}
128-
129118
const originalMsgHash = hashTypedData(typedData);
130119
// check if the account contract supports EIP721 domain separator based signing
131-
let factorySupports712 = false;
132-
try {
133-
// this will throw if the contract does not support it (old factories)
134-
await readContract({
135-
contract: accountContract,
136-
method:
137-
"function getMessageHash(bytes32 _hash) public view returns (bytes32)",
138-
params: [originalMsgHash],
139-
});
140-
factorySupports712 = true;
141-
} catch {
142-
// ignore
143-
}
120+
const is712Factory = await checkFor712Factory({
121+
factoryContract,
122+
accountContract,
123+
originalMsgHash,
124+
});
144125

145126
let sig: `0x${string}`;
146-
if (factorySupports712) {
127+
if (is712Factory) {
147128
const wrappedMessageHash = encodeAbiParameters(
148129
[{ type: "bytes32" }],
149130
[originalMsgHash],
@@ -163,46 +144,39 @@ export async function deployAndSignTypedData<
163144
sig = await options.personalAccount.signTypedData(typedData);
164145
}
165146

147+
const deployTx = prepareCreateAccount({
148+
factoryContract,
149+
adminAddress: options.personalAccount.address,
150+
accountSalt: options.overrides?.accountSalt,
151+
createAccountOverride: options.overrides?.createAccount,
152+
});
153+
if (!deployTx) {
154+
throw new Error("Create account override not provided");
155+
}
156+
const initCode = await encode(deployTx);
157+
const erc6492Sig = serializeErc6492Signature({
158+
address: factoryContract.address,
159+
data: initCode,
160+
signature: sig,
161+
});
162+
163+
// check if the signature is valid
166164
const isValid = await verifyHash({
167165
hash: originalMsgHash,
168-
signature: sig,
166+
signature: erc6492Sig,
169167
address: accountContract.address,
170-
chain: options.chain,
171-
client: options.client,
168+
chain: accountContract.chain,
169+
client: accountContract.client,
172170
});
173171

174172
if (isValid) {
175-
return sig;
173+
return erc6492Sig;
176174
}
177175
throw new Error(
178-
"Unable to verify signature on smart account, please make sure the smart account is deployed and the signature is valid.",
176+
"Unable to verify signature on smart account, please make sure the admin wallet has permissions and the signature is valid.",
179177
);
180178
}
181179

182-
async function _deployAccount(args: {
183-
options: SmartAccountOptions;
184-
account: Account;
185-
accountContract: ThirdwebContract;
186-
}) {
187-
const { options, account, accountContract } = args;
188-
const [{ sendTransaction }, { prepareTransaction }] = await Promise.all([
189-
import("../../../transaction/actions/send-transaction.js"),
190-
import("../../../transaction/prepare-transaction.js"),
191-
]);
192-
const dummyTx = prepareTransaction({
193-
client: options.client,
194-
chain: options.chain,
195-
to: accountContract.address,
196-
value: 0n,
197-
gas: 50000n, // force gas to avoid simulation error
198-
});
199-
const deployResult = await sendTransaction({
200-
transaction: dummyTx,
201-
account,
202-
});
203-
return deployResult;
204-
}
205-
206180
export async function confirmContractDeployment(args: {
207181
accountContract: ThirdwebContract;
208182
}) {
@@ -223,3 +197,37 @@ export async function confirmContractDeployment(args: {
223197
isDeployed = await isContractDeployed(accountContract);
224198
}
225199
}
200+
201+
async function checkFor712Factory({
202+
factoryContract,
203+
accountContract,
204+
originalMsgHash,
205+
}: {
206+
factoryContract: ThirdwebContract;
207+
accountContract: ThirdwebContract;
208+
originalMsgHash: Hex.Hex;
209+
}) {
210+
try {
211+
const implementationAccount = await readContract({
212+
contract: factoryContract,
213+
method: "function accountImplementation() public view returns (address)",
214+
});
215+
// check if the account contract supports EIP721 domain separator or modular based signing
216+
const is712Factory = await readContract({
217+
contract: getContract({
218+
address: implementationAccount,
219+
chain: accountContract.chain,
220+
client: accountContract.client,
221+
}),
222+
method:
223+
"function getMessageHash(bytes32 _hash) public view returns (bytes32)",
224+
params: [originalMsgHash],
225+
})
226+
.then((res) => res !== "0x")
227+
.catch(() => false);
228+
229+
return is712Factory;
230+
} catch {
231+
return false;
232+
}
233+
}

0 commit comments

Comments
 (0)