Skip to content

Commit e7528af

Browse files
authored
[Smart Wallet] Always force deploy on sign/auth, support 1271 (#2341)
1 parent a50d3b0 commit e7528af

File tree

8 files changed

+128
-96
lines changed

8 files changed

+128
-96
lines changed

.changeset/good-ways-listen.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@thirdweb-dev/unity-js-bridge": minor
3+
"@thirdweb-dev/wallets": patch
4+
---
5+
6+
[Smart Wallet] Always force deploy on sign/auth, use 712 with 1271 where possible

packages/unity-js-bridge/src/thirdweb-bridge.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ class ThirdwebBridge implements TWBridge {
144144
}
145145
(globalThis as any).X_SDK_NAME = "UnitySDK_WebGL";
146146
(globalThis as any).X_SDK_PLATFORM = "unity";
147-
(globalThis as any).X_SDK_VERSION = "4.6.4";
147+
(globalThis as any).X_SDK_VERSION = "4.7.0";
148148
(globalThis as any).X_SDK_OS = browser?.os ?? "unknown";
149149
}
150150
this.initializedChain = chain;
@@ -244,7 +244,6 @@ class ThirdwebBridge implements TWBridge {
244244
paymasterUrl: sdkOptions.smartWalletConfig?.paymasterUrl,
245245
// paymasterAPI: sdkOptions.smartWalletConfig?.paymasterAPI,
246246
entryPointAddress: sdkOptions.smartWalletConfig?.entryPointAddress,
247-
deployOnSign: sdkOptions.smartWalletConfig?.deployOnSign,
248247
};
249248
walletInstance = new SmartWallet(config);
250249
break;
@@ -813,18 +812,19 @@ class ThirdwebBridge implements TWBridge {
813812
if (!this.activeWallet) {
814813
throw new Error("No wallet connected");
815814
}
816-
try{
815+
try {
817816
const smartWallet = this.activeWallet as SmartWallet;
818817
const signer = await smartWallet.getPersonalWallet()?.getSigner();
819818
const res = await signer?.getAddress();
820819
return JSON.stringify({ result: res }, bigNumberReplacer);
821820
} catch {
822-
console.debug("Could not find a smart wallet, defaulting to normal signer");
821+
console.debug(
822+
"Could not find a smart wallet, defaulting to normal signer",
823+
);
823824
const signer = await this.activeWallet.getSigner();
824825
const res = await signer.getAddress();
825826
return JSON.stringify({ result: res }, bigNumberReplacer);
826827
}
827-
828828
}
829829

830830
public openPopupWindow() {

packages/wallets/src/evm/connectors/smart-wallet/index.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
5454
this.config.paymasterUrl ||
5555
`https://${this.chainId}.bundler.thirdweb.com/v2`;
5656
const entryPointAddress = config.entryPointAddress || ENTRYPOINT_ADDRESS;
57-
const deployOnSign = config.deployOnSign ?? true;
5857
const localSigner = await params.personalWallet.getSigner();
5958
const providerConfig: ProviderConfig = {
6059
chain: config.chain,
@@ -70,7 +69,6 @@ export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
7069
this.config.secretKey,
7170
),
7271
gasless: config.gasless,
73-
deployOnSign: deployOnSign,
7472
factoryAddress: config.factoryAddress,
7573
accountAddress: params.accountAddress,
7674
factoryInfo: config.factoryInfo || this.defaultFactoryInfo(),
@@ -316,8 +314,8 @@ export class SmartWalletConnector extends Connector<SmartWalletConnectionArgs> {
316314
value: await transaction.getValue(),
317315
gasLimit: await transaction.getOverrides().gasLimit,
318316
maxFeePerGas: await transaction.getOverrides().maxFeePerGas,
319-
maxPriorityFeePerGas: await transaction.getOverrides()
320-
.maxPriorityFeePerGas,
317+
maxPriorityFeePerGas:
318+
await transaction.getOverrides().maxPriorityFeePerGas,
321319
nonce: await transaction.getOverrides().nonce,
322320
},
323321
options,

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

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
import { ethers, providers, utils } from "ethers";
1+
import { Contract, ethers, providers, utils } from "ethers";
22

33
import { Bytes, Signer } from "ethers";
44
import { BaseAccountAPI } from "./base-api";
55
import type { ERC4337EthersProvider } from "./erc4337-provider";
66
import { HttpRpcClient } from "./http-rpc-client";
77
import { hexlifyUserOp, randomNonce } from "./utils";
88
import { ProviderConfig, UserOpOptions } from "../types";
9+
import { signTypedDataInternal } from "@thirdweb-dev/sdk";
10+
import {
11+
checkContractWalletSignature,
12+
chainIdToThirdwebRpc,
13+
} from "../../../wallets/abstract";
914

1015
export class ERC4337EthersSigner extends Signer {
1116
config: ProviderConfig;
@@ -138,20 +143,84 @@ Code: ${errorCode}`;
138143
return this.address as string;
139144
}
140145

141-
async signMessage(message: Bytes | string): Promise<string> {
142-
const isNotDeployed = await this.smartAccountAPI.checkAccountPhantom();
143-
if (isNotDeployed && this.config.deployOnSign) {
144-
console.log(
145-
"Account contract not deployed yet. Deploying account before signing message",
146-
);
147-
const tx = await this.sendTransaction({
148-
to: await this.getAddress(),
149-
data: "0x",
150-
});
151-
await tx.wait();
152-
}
153-
154-
return await this.originalSigner.signMessage(message);
146+
/**
147+
* Sign a message and return the signature
148+
*/
149+
public async signMessage(message: Bytes | string): Promise<string> {
150+
// Deploy smart wallet if needed
151+
const isNotDeployed = await this.smartAccountAPI.checkAccountPhantom();
152+
if (isNotDeployed) {
153+
console.log(
154+
"Account contract not deployed yet. Deploying account before signing message",
155+
);
156+
const tx = await this.sendTransaction({
157+
to: await this.getAddress(),
158+
data: "0x",
159+
});
160+
await tx.wait();
161+
}
162+
163+
const [chainId, address] = await Promise.all([
164+
this.getChainId(),
165+
this.getAddress(),
166+
]);
167+
const originalMsgHash = utils.hashMessage(message);
168+
169+
let factorySupports712: boolean;
170+
let signature: string;
171+
172+
try {
173+
const provider = new providers.JsonRpcProvider(
174+
chainIdToThirdwebRpc(chainId, this.config.clientId),
175+
chainId,
176+
);
177+
const walletContract = new Contract(
178+
address,
179+
[
180+
"function getMessageHash(bytes32 _hash) public view returns (bytes32)",
181+
],
182+
provider,
183+
);
184+
// if this fails it's a pre 712 factory
185+
await walletContract.getMessageHash(originalMsgHash);
186+
factorySupports712 = true;
187+
} catch {
188+
factorySupports712 = false;
189+
}
190+
191+
if (factorySupports712) {
192+
const result = await signTypedDataInternal(
193+
this,
194+
{
195+
name: "Account",
196+
version: "1",
197+
chainId,
198+
verifyingContract: address,
199+
},
200+
{ AccountMessage: [{ name: "message", type: "bytes" }] },
201+
{
202+
message: utils.defaultAbiCoder.encode(["bytes32"], [originalMsgHash]),
203+
},
204+
);
205+
signature = result.signature;
206+
} else {
207+
signature = await this.originalSigner.signMessage(message);
208+
}
209+
210+
const isValid = await checkContractWalletSignature(
211+
message as string,
212+
signature,
213+
address,
214+
chainId,
215+
);
216+
217+
if (isValid) {
218+
return signature;
219+
} else {
220+
throw new Error(
221+
"Unable to verify signature on smart account, please make sure the smart account is deployed and the signature is valid.",
222+
);
223+
}
155224
}
156225

157226
async signTransaction(

packages/wallets/src/evm/connectors/smart-wallet/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export type SmartWalletConfig = {
2222
paymasterUrl?: string;
2323
paymasterAPI?: PaymasterAPI;
2424
entryPointAddress?: string;
25-
deployOnSign?: boolean;
2625
} & ContractInfoInput &
2726
WalletConnectReceiverConfig;
2827

@@ -43,7 +42,6 @@ export interface ProviderConfig extends ContractInfo {
4342
accountAddress?: string;
4443
paymasterAPI: PaymasterAPI;
4544
gasless: boolean;
46-
deployOnSign?: boolean;
4745
}
4846

4947
export type ContractInfoInput = {

packages/wallets/src/evm/wallets/abstract.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
import { createErc20 } from "../utils/currency";
1616

1717
// TODO improve this
18-
function chainIdToThirdwebRpc(chainId: number, clientId?: string) {
18+
export function chainIdToThirdwebRpc(chainId: number, clientId?: string) {
1919
return `https://${chainId}.rpc.thirdweb.com${clientId ? `/${clientId}` : ""}${
2020
typeof globalThis !== "undefined" && "APP_BUNDLE_ID" in globalThis
2121
? `?bundleId=${(globalThis as any).APP_BUNDLE_ID as string}`
@@ -66,9 +66,11 @@ export async function checkContractWalletSignature(
6666
skipFetchSetup: _skipFetchSetup,
6767
});
6868
const walletContract = new Contract(address, EIP1271_ABI, provider);
69-
const _hashMessage = utils.hashMessage(message);
7069
try {
71-
const res = await walletContract.isValidSignature(_hashMessage, signature);
70+
const res = await walletContract.isValidSignature(
71+
utils.hashMessage(message),
72+
signature,
73+
);
7274
return res === EIP1271_MAGICVALUE;
7375
} catch {
7476
return false;

packages/wallets/src/evm/wallets/smart-wallet.ts

Lines changed: 3 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { AbstractClientWallet, WalletOptions } from "./base";
2-
import { checkContractWalletSignature } from "./abstract";
32
import type { ConnectParams } from "../interfaces/connector";
43
import type {
54
SmartWalletConfig,
@@ -16,8 +15,7 @@ import {
1615
} from "@thirdweb-dev/sdk";
1716
import { walletIds } from "../constants/walletIds";
1817
import { getValidChainRPCs } from "@thirdweb-dev/chains";
19-
import { providers, utils, Bytes, Signer } from "ethers";
20-
import { signTypedDataInternal } from "@thirdweb-dev/sdk";
18+
import { providers, utils } from "ethers";
2119

2220
// export types and utils for convenience
2321
export type * from "../connectors/smart-wallet/types";
@@ -246,10 +244,10 @@ export class SmartWallet extends AbstractClientWallet<
246244
* The entrypoint contract address. Uses v0.6 by default.
247245
*
248246
* Must be a `string`.
249-
*
247+
*
250248
* #### deployOnSign
251249
* Whether to deploy the smart wallet when the user signs a message. Defaults to true.
252-
*
250+
*
253251
* Must be a `boolean`.
254252
*
255253
* #### chains
@@ -316,68 +314,6 @@ export class SmartWallet extends AbstractClientWallet<
316314
return this.connector?.personalWallet;
317315
}
318316

319-
/**
320-
* Sign a message and return the signature
321-
*/
322-
public async signMessage(message: Bytes | string): Promise<string> {
323-
// Deploy smart wallet if needed
324-
const connector = await this.getConnector();
325-
await connector.deployIfNeeded();
326-
327-
const erc4337Signer = await this.getSigner();
328-
const chainId = await erc4337Signer.getChainId();
329-
const address = await connector.getAddress();
330-
331-
/**
332-
* We first try to sign the EIP-712 typed data i.e. the message mixed with the smart wallet's domain separator.
333-
* If this fails, we fallback to the legacy signing method.
334-
*/
335-
try {
336-
const result = await signTypedDataInternal(
337-
erc4337Signer,
338-
{
339-
name: "Account",
340-
version: "1",
341-
chainId,
342-
verifyingContract: address,
343-
},
344-
{ AccountMessage: [{ name: "message", type: "bytes" }] },
345-
{
346-
message: utils.defaultAbiCoder.encode(
347-
["bytes32"],
348-
[utils.hashMessage(message)],
349-
),
350-
},
351-
);
352-
353-
const isValid = await checkContractWalletSignature(
354-
message as string,
355-
result.signature,
356-
address,
357-
chainId,
358-
);
359-
360-
if (!isValid) {
361-
throw new Error("Invalid signature");
362-
}
363-
364-
return result.signature;
365-
} catch {
366-
return await this.signMessageLegacy(erc4337Signer, message);
367-
}
368-
}
369-
370-
/**
371-
* This is only for for legacy EIP-1271 signature verification
372-
* Sign a message and return the signature
373-
*/
374-
private async signMessageLegacy(
375-
signer: Signer,
376-
message: Bytes | string,
377-
): Promise<string> {
378-
return await signer.signMessage(message);
379-
}
380-
381317
/**
382318
* Check whether the connected signer can execute a given transaction using the smart wallet.
383319
* @param transaction - The transaction to execute using the smart wallet.

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { SmartWallet } from "../src/evm/wallets/smart-wallet";
33
import { LocalWallet } from "../src/evm/wallets/local-wallet";
44
import { Mumbai } from "@thirdweb-dev/chains";
55
import { ThirdwebSDK, SmartContract } from "@thirdweb-dev/sdk";
6+
import { checkContractWalletSignature } from "../src/evm/wallets/abstract";
7+
8+
require("dotenv-mono").load();
9+
jest.setTimeout(240_000);
610

711
let smartWallet: SmartWallet;
812
let smartWalletAddress: string;
@@ -93,4 +97,23 @@ describeIf(!!process.env.TW_SECRET_KEY)("SmartWallet core tests", () => {
9397
const balance = await contract.erc1155.balance(0);
9498
expect(balance.toNumber()).toEqual(7);
9599
});
100+
101+
it("can sign and verify 1271", async () => {
102+
const message = "0x1234";
103+
const sig = await smartWallet.signMessage(message);
104+
const isValidV1 = await smartWallet.verifySignature(
105+
message,
106+
sig,
107+
smartWalletAddress,
108+
chain.chainId,
109+
);
110+
expect(isValidV1).toEqual(true);
111+
const isValidV2 = await checkContractWalletSignature(
112+
message,
113+
sig,
114+
smartWalletAddress,
115+
chain.chainId,
116+
);
117+
expect(isValidV2).toEqual(true);
118+
});
96119
});

0 commit comments

Comments
 (0)