From 4a5b4b4d68ff15111fd05d602f54259055ab785d Mon Sep 17 00:00:00 2001 From: gregfromstl Date: Fri, 27 Dec 2024 12:10:44 -0600 Subject: [PATCH] feat(sdk): adds eagerDeployment option to smart wallets feat(sdk): add eagerDeployment option to smart wallets --- .changeset/polite-trains-kick.md | 15 ++ packages/thirdweb/src/auth/verify-hash.ts | 21 +- .../thirdweb/src/wallets/create-wallet.ts | 16 ++ packages/thirdweb/src/wallets/smart/index.ts | 10 +- .../thirdweb/src/wallets/smart/lib/signing.ts | 214 +++++++++++++----- .../smart-wallet-integration-v07.test.ts | 80 ++++++- .../smart/smart-wallet-integration.test.ts | 78 ++++++- .../src/wallets/smart/smart-wallet.ts | 14 ++ packages/thirdweb/src/wallets/smart/types.ts | 1 + 9 files changed, 377 insertions(+), 72 deletions(-) create mode 100644 .changeset/polite-trains-kick.md diff --git a/.changeset/polite-trains-kick.md b/.changeset/polite-trains-kick.md new file mode 100644 index 00000000000..82f899e886f --- /dev/null +++ b/.changeset/polite-trains-kick.md @@ -0,0 +1,15 @@ +--- +"thirdweb": minor +--- + +Feature: Adds eagerDeployment option for smart accounts that need EIP-1271 signatures. + +When setting `eagerDeployment` to `true`, smart accounts will use the legacy behavior of deploying prior to signing a message or typed data. + +```ts +const wallet = smartWallet({ + chain, + gasless: true, + eagerDeployment: true, +}); +``` diff --git a/packages/thirdweb/src/auth/verify-hash.ts b/packages/thirdweb/src/auth/verify-hash.ts index 51f20d5201d..da32a454065 100644 --- a/packages/thirdweb/src/auth/verify-hash.ts +++ b/packages/thirdweb/src/auth/verify-hash.ts @@ -129,8 +129,7 @@ export async function verifyHash({ try { const result = await eth_call(rpcRequest, verificationData); return hexToBool(result); - } catch (err) { - console.error("Error verifying ERC-6492 signature", err); + } catch { // Some chains do not support the eth_call simulation and will fail, so we fall back to regular EIP1271 validation const validEip1271 = await verifyEip1271Signature({ hash, @@ -154,7 +153,7 @@ export async function verifyHash({ } const EIP_1271_MAGIC_VALUE = "0x1626ba7e"; -async function verifyEip1271Signature({ +export async function verifyEip1271Signature({ hash, signature, contract, @@ -163,10 +162,14 @@ async function verifyEip1271Signature({ signature: Hex; contract: ThirdwebContract; }): Promise { - const result = await isValidSignature({ - hash, - signature, - contract, - }); - return result === EIP_1271_MAGIC_VALUE; + try { + const result = await isValidSignature({ + hash, + signature, + contract, + }); + return result === EIP_1271_MAGIC_VALUE; + } catch { + return false; + } } diff --git a/packages/thirdweb/src/wallets/create-wallet.ts b/packages/thirdweb/src/wallets/create-wallet.ts index 132244b006e..2b290076c7a 100644 --- a/packages/thirdweb/src/wallets/create-wallet.ts +++ b/packages/thirdweb/src/wallets/create-wallet.ts @@ -116,6 +116,22 @@ import { createWalletEmitter } from "./wallet-emitter.js"; * * [View Coinbase wallet creation options](https://portal.thirdweb.com/references/typescript/v5/CoinbaseWalletCreationOptions) * + * ## Connecting with a smart wallet + * + * ```ts + * import { createWallet } from "thirdweb/wallets"; + * + * const wallet = createWallet("smart", { + * chain: sepolia, + * sponsorGas: true, + * }); + * + * const account = await wallet.connect({ + * client, + * personalAccount, // pass the admin account + * }); + * ``` + * * @wallet */ export function createWallet( diff --git a/packages/thirdweb/src/wallets/smart/index.ts b/packages/thirdweb/src/wallets/smart/index.ts index f97d8fb386c..1ef12c56fc5 100644 --- a/packages/thirdweb/src/wallets/smart/index.ts +++ b/packages/thirdweb/src/wallets/smart/index.ts @@ -277,8 +277,9 @@ async function createSmartAccount( }); } - const { deployAndSignMessage } = await import("./lib/signing.js"); - return deployAndSignMessage({ + const { smartAccountSignMessage } = await import("./lib/signing.js"); + return smartAccountSignMessage({ + smartAccount: account, accountContract, factoryContract: options.factoryContract, options, @@ -298,8 +299,9 @@ async function createSmartAccount( }); } - const { deployAndSignTypedData } = await import("./lib/signing.js"); - return deployAndSignTypedData({ + const { smartAccountSignTypedData } = await import("./lib/signing.js"); + return smartAccountSignTypedData({ + smartAccount: account, accountContract, factoryContract: options.factoryContract, options, diff --git a/packages/thirdweb/src/wallets/smart/lib/signing.ts b/packages/thirdweb/src/wallets/smart/lib/signing.ts index e5f81c297af..1570ad93183 100644 --- a/packages/thirdweb/src/wallets/smart/lib/signing.ts +++ b/packages/thirdweb/src/wallets/smart/lib/signing.ts @@ -1,6 +1,11 @@ import type * as ox__TypedData from "ox/TypedData"; import { serializeErc6492Signature } from "../../../auth/serialize-erc6492-signature.js"; -import { verifyHash } from "../../../auth/verify-hash.js"; +import { + verifyEip1271Signature, + verifyHash, +} from "../../../auth/verify-hash.js"; +import type { Chain } from "../../../chains/types.js"; +import type { ThirdwebClient } from "../../../client/client.js"; import { type ThirdwebContract, getContract, @@ -8,19 +13,29 @@ import { import { encode } from "../../../transaction/actions/encode.js"; import { readContract } from "../../../transaction/read-contract.js"; import { encodeAbiParameters } from "../../../utils/abi/encodeAbiParameters.js"; +import { isContractDeployed } from "../../../utils/bytecode/is-contract-deployed.js"; import type { Hex } from "../../../utils/encoding/hex.js"; import { hashMessage } from "../../../utils/hashing/hashMessage.js"; import { hashTypedData } from "../../../utils/hashing/hashTypedData.js"; import type { SignableMessage } from "../../../utils/types.js"; +import type { Account } from "../../../wallets/interfaces/wallet.js"; import type { SmartAccountOptions } from "../types.js"; import { prepareCreateAccount } from "./calls.js"; -export async function deployAndSignMessage({ +/** + * If the account is already deployed, generate an ERC-1271 signature. + * If the account is not deployed, generate an ERC-6492 signature unless otherwise specified. + * + * @internal + */ +export async function smartAccountSignMessage({ + smartAccount, accountContract, factoryContract, options, message, }: { + smartAccount: Account; accountContract: ThirdwebContract; factoryContract: ThirdwebContract; options: SmartAccountOptions; @@ -55,48 +70,73 @@ export async function deployAndSignMessage({ sig = await options.personalAccount.signMessage({ message }); } - const deployTx = prepareCreateAccount({ - factoryContract, - adminAddress: options.personalAccount.address, - accountSalt: options.overrides?.accountSalt, - createAccountOverride: options.overrides?.createAccount, - }); - if (!deployTx) { - throw new Error("Create account override not provided"); + if (options.eagerDeployment) { + await forceDeploySmartAccount({ + chain: options.chain, + client: options.client, + smartAccount, + accountContract, + }); + await confirmContractDeployment({ + accountContract, + }); } - const initCode = await encode(deployTx); - const erc6492Sig = serializeErc6492Signature({ - address: factoryContract.address, - data: initCode, - signature: sig, - }); - // check if the signature is valid - const isValid = await verifyHash({ - hash: originalMsgHash, - signature: erc6492Sig, - address: accountContract.address, - chain: accountContract.chain, - client: accountContract.client, - }); + const isDeployed = await isContractDeployed(accountContract); + if (isDeployed) { + const isValid = await verifyEip1271Signature({ + hash: originalMsgHash, + signature: sig, + contract: accountContract, + }); + if (isValid) { + return sig; + } + throw new Error("Failed to verify signature"); + } else { + const deployTx = prepareCreateAccount({ + factoryContract, + adminAddress: options.personalAccount.address, + accountSalt: options.overrides?.accountSalt, + createAccountOverride: options.overrides?.createAccount, + }); + if (!deployTx) { + throw new Error("Create account override not provided"); + } + const initCode = await encode(deployTx); + const erc6492Sig = serializeErc6492Signature({ + address: factoryContract.address, + data: initCode, + signature: sig, + }); - if (isValid) { - return erc6492Sig; + // check if the signature is valid + const isValid = await verifyHash({ + hash: originalMsgHash, + signature: erc6492Sig, + address: accountContract.address, + chain: accountContract.chain, + client: accountContract.client, + }); + + if (isValid) { + return erc6492Sig; + } + throw new Error("Unable to verify ERC-6492 signature after signing."); } - throw new Error( - "Unable to verify signature on smart account, please make sure the admin wallet has permissions and the signature is valid.", - ); } -export async function deployAndSignTypedData< +export async function smartAccountSignTypedData< const typedData extends ox__TypedData.TypedData | Record, primaryType extends keyof typedData | "EIP712Domain" = keyof typedData, >({ + smartAccount, accountContract, factoryContract, options, typedData, }: { + smartAccount: Account; accountContract: ThirdwebContract; factoryContract: ThirdwebContract; options: SmartAccountOptions; @@ -142,37 +182,62 @@ export async function deployAndSignTypedData< sig = await options.personalAccount.signTypedData(typedData); } - const deployTx = prepareCreateAccount({ - factoryContract, - adminAddress: options.personalAccount.address, - accountSalt: options.overrides?.accountSalt, - createAccountOverride: options.overrides?.createAccount, - }); - if (!deployTx) { - throw new Error("Create account override not provided"); + if (options.eagerDeployment) { + await forceDeploySmartAccount({ + chain: options.chain, + client: options.client, + smartAccount, + accountContract, + }); + await confirmContractDeployment({ + accountContract, + }); } - const initCode = await encode(deployTx); - const erc6492Sig = serializeErc6492Signature({ - address: factoryContract.address, - data: initCode, - signature: sig, - }); - // check if the signature is valid - const isValid = await verifyHash({ - hash: originalMsgHash, - signature: erc6492Sig, - address: accountContract.address, - chain: accountContract.chain, - client: accountContract.client, - }); + const isDeployed = await isContractDeployed(accountContract); + if (isDeployed) { + const isValid = await verifyEip1271Signature({ + hash: originalMsgHash, + signature: sig, + contract: accountContract, + }); + if (isValid) { + return sig; + } + throw new Error("Failed to verify signature"); + } else { + const deployTx = prepareCreateAccount({ + factoryContract, + adminAddress: options.personalAccount.address, + accountSalt: options.overrides?.accountSalt, + createAccountOverride: options.overrides?.createAccount, + }); + if (!deployTx) { + throw new Error("Create account override not provided"); + } + const initCode = await encode(deployTx); + const erc6492Sig = serializeErc6492Signature({ + address: factoryContract.address, + data: initCode, + signature: sig, + }); - if (isValid) { - return erc6492Sig; + // check if the signature is valid + const isValid = await verifyHash({ + hash: originalMsgHash, + signature: erc6492Sig, + address: accountContract.address, + chain: accountContract.chain, + client: accountContract.client, + }); + + if (isValid) { + return erc6492Sig; + } + throw new Error( + "Unable to verify signature on smart account, please make sure the admin wallet has permissions and the signature is valid.", + ); } - throw new Error( - "Unable to verify signature on smart account, please make sure the admin wallet has permissions and the signature is valid.", - ); } export async function confirmContractDeployment(args: { @@ -229,3 +294,38 @@ async function checkFor712Factory({ return false; } } + +/** + * Forces smart account deployment via a dummy transaction + * + * @internal + */ +export async function forceDeploySmartAccount(args: { + smartAccount: Account; + chain: Chain; + client: ThirdwebClient; + accountContract: ThirdwebContract; +}) { + const { chain, client, smartAccount, accountContract } = args; + const isDeployed = await isContractDeployed(accountContract); + if (isDeployed) { + return; + } + + const [{ sendTransaction }, { prepareTransaction }] = await Promise.all([ + import("../../../transaction/actions/send-transaction.js"), + import("../../../transaction/prepare-transaction.js"), + ]); + const dummyTx = prepareTransaction({ + client: client, + chain: chain, + to: accountContract.address, + value: 0n, + gas: 50000n, // force gas to avoid simulation error + }); + const deployResult = await sendTransaction({ + transaction: dummyTx, + account: smartAccount, + }); + return deployResult; +} diff --git a/packages/thirdweb/src/wallets/smart/smart-wallet-integration-v07.test.ts b/packages/thirdweb/src/wallets/smart/smart-wallet-integration-v07.test.ts index 015bb6cf8b2..88885275498 100644 --- a/packages/thirdweb/src/wallets/smart/smart-wallet-integration-v07.test.ts +++ b/packages/thirdweb/src/wallets/smart/smart-wallet-integration-v07.test.ts @@ -5,6 +5,7 @@ import { verifySignature } from "../../auth/verify-signature.js"; import { type ThirdwebContract, getContract } from "../../contract/contract.js"; import { parseEventLogs } from "../../event/actions/parse-logs.js"; +import { verifyEip1271Signature } from "../../auth/verify-hash.js"; import { verifyTypedData } from "../../auth/verify-typed-data.js"; import { sepolia } from "../../chains/chain-definitions/sepolia.js"; import { @@ -20,12 +21,17 @@ import { sendBatchTransaction } from "../../transaction/actions/send-batch-trans import { waitForReceipt } from "../../transaction/actions/wait-for-tx-receipt.js"; import { getAddress } from "../../utils/address.js"; import { isContractDeployed } from "../../utils/bytecode/is-contract-deployed.js"; +import { hashMessage } from "../../utils/hashing/hashMessage.js"; +import { hashTypedData } from "../../utils/hashing/hashTypedData.js"; import { sleep } from "../../utils/sleep.js"; import type { Account, Wallet } from "../interfaces/wallet.js"; import { generateAccount } from "../utils/generateAccount.js"; import { predictSmartAccountAddress } from "./lib/calls.js"; import { DEFAULT_ACCOUNT_FACTORY_V0_7 } from "./lib/constants.js"; -import { confirmContractDeployment } from "./lib/signing.js"; +import { + confirmContractDeployment, + forceDeploySmartAccount, +} from "./lib/signing.js"; import { smartWallet } from "./smart-wallet.js"; let wallet: Wallet; @@ -95,6 +101,57 @@ describe.runIf(process.env.TW_SECRET_KEY)( expect(isValid).toEqual(true); }); + it("should force deployment on signature if eagerDeployment is true", async () => { + const eagerDeploymentWallet = smartWallet({ + chain, + gasless: true, + eagerDeployment: true, + }); + const smartAccount = await eagerDeploymentWallet.connect({ + client: TEST_CLIENT, + personalAccount, + }); + const smartWalletAddress = smartAccount.address; + const accountContract = getContract({ + address: smartWalletAddress, + chain, + client, + }); + const signature = await smartAccount.signMessage({ + message: "hello world", + }); + const isDeployed = await isContractDeployed(accountContract); + expect(isDeployed).toEqual(true); + + const isValidSignature = await verifyEip1271Signature({ + hash: hashMessage("hello world"), + signature, + contract: accountContract, + }); + expect(isValidSignature).toEqual(true); + }); + + it("should use ERC-1271 signatures after deployment", async () => { + await forceDeploySmartAccount({ + chain, + client, + smartAccount, + accountContract, + }); + await new Promise((resolve) => setTimeout(resolve, 1000)); // pause for a second to prevent race condition + + const signature = await smartAccount.signMessage({ + message: "hello world", + }); + + const isValid = await verifyEip1271Signature({ + hash: hashMessage("hello world"), + signature, + contract: accountContract, + }); + expect(isValid).toEqual(true); + }); + it("can sign typed data", async () => { const signature = await smartAccount.signTypedData(typedData.basic); const isValid = await verifyTypedData({ @@ -107,6 +164,27 @@ describe.runIf(process.env.TW_SECRET_KEY)( expect(isValid).toEqual(true); }); + it("should use ERC-1271 typed data signatures after deployment", async () => { + await forceDeploySmartAccount({ + chain, + client, + smartAccount, + accountContract, + }); + + await new Promise((resolve) => setTimeout(resolve, 1000)); // pause for a second to prevent race condition + + const signature = await smartAccount.signTypedData(typedData.basic); + + const messageHash = hashTypedData(typedData.basic); + const isValid = await verifyEip1271Signature({ + signature, + hash: messageHash, + contract: accountContract, + }); + expect(isValid).toEqual(true); + }); + it("should revert on unsuccessful transactions", async () => { const tx = sendAndConfirmTransaction({ transaction: setContractURI({ diff --git a/packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts b/packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts index 012b936d319..a8061d0102b 100644 --- a/packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts +++ b/packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts @@ -1,6 +1,7 @@ import { beforeAll, describe, expect, it } from "vitest"; import { TEST_CLIENT } from "../../../test/src/test-clients.js"; import { typedData } from "../../../test/src/typed-data.js"; +import { verifyEip1271Signature } from "../../auth/verify-hash.js"; import { verifySignature } from "../../auth/verify-signature.js"; import { verifyTypedData } from "../../auth/verify-typed-data.js"; import { arbitrumSepolia } from "../../chains/chain-definitions/arbitrum-sepolia.js"; @@ -21,10 +22,13 @@ import { sendTransaction } from "../../transaction/actions/send-transaction.js"; import { waitForReceipt } from "../../transaction/actions/wait-for-tx-receipt.js"; import { prepareTransaction } from "../../transaction/prepare-transaction.js"; import { isContractDeployed } from "../../utils/bytecode/is-contract-deployed.js"; +import { hashMessage } from "../../utils/hashing/hashMessage.js"; +import { hashTypedData } from "../../utils/hashing/hashTypedData.js"; import { sleep } from "../../utils/sleep.js"; import type { Account, Wallet } from "../interfaces/wallet.js"; import { generateAccount } from "../utils/generateAccount.js"; import { predictSmartAccountAddress } from "./lib/calls.js"; +import { forceDeploySmartAccount } from "./lib/signing.js"; import { smartWallet } from "./smart-wallet.js"; let wallet: Wallet; @@ -93,6 +97,57 @@ describe.runIf(process.env.TW_SECRET_KEY).sequential( expect(isValid).toEqual(true); }); + it("should force deployment on signature if eagerDeployment is true", async () => { + const eagerDeploymentWallet = smartWallet({ + chain, + gasless: true, + eagerDeployment: true, + }); + const smartAccount = await eagerDeploymentWallet.connect({ + client: TEST_CLIENT, + personalAccount, + }); + const smartWalletAddress = smartAccount.address; + const accountContract = getContract({ + address: smartWalletAddress, + chain, + client, + }); + const signature = await smartAccount.signMessage({ + message: "hello world", + }); + const isDeployed = await isContractDeployed(accountContract); + expect(isDeployed).toEqual(true); + + const isValidSignature = await verifyEip1271Signature({ + hash: hashMessage("hello world"), + signature, + contract: accountContract, + }); + expect(isValidSignature).toEqual(true); + }); + + it("should use ERC-1271 signatures after deployment", async () => { + await forceDeploySmartAccount({ + chain, + client, + smartAccount, + accountContract, + }); + await new Promise((resolve) => setTimeout(resolve, 1000)); // pause for a second to prevent race condition + + const signature = await smartAccount.signMessage({ + message: "hello world", + }); + + const isValid = await verifyEip1271Signature({ + hash: hashMessage("hello world"), + signature, + contract: accountContract, + }); + expect(isValid).toEqual(true); + }); + it("can sign typed data", async () => { const signature = await smartAccount.signTypedData(typedData.basic); const isValid = await verifyTypedData({ @@ -105,6 +160,27 @@ describe.runIf(process.env.TW_SECRET_KEY).sequential( expect(isValid).toEqual(true); }); + it("should use ERC-1271 typed data signatures after deployment", async () => { + await forceDeploySmartAccount({ + chain, + client, + smartAccount, + accountContract, + }); + + await new Promise((resolve) => setTimeout(resolve, 1000)); // pause for a second to prevent race condition + + const signature = await smartAccount.signTypedData(typedData.basic); + + const messageHash = hashTypedData(typedData.basic); + const isValid = await verifyEip1271Signature({ + signature, + hash: messageHash, + contract: accountContract, + }); + expect(isValid).toEqual(true); + }); + it("should revert on unsuccessful transactions", async () => { const tx = sendAndConfirmTransaction({ transaction: setContractURI({ @@ -183,7 +259,7 @@ describe.runIf(process.env.TW_SECRET_KEY).sequential( expect(result.status).toEqual("success"); }); - it("can sign and verify 1271 with replay protection", async () => { + it("can sign and verify with replay protection", async () => { const message = "hello world"; const signature = await smartAccount.signMessage({ message }); const isValidV1 = await verifySignature({ diff --git a/packages/thirdweb/src/wallets/smart/smart-wallet.ts b/packages/thirdweb/src/wallets/smart/smart-wallet.ts index 5110b9dd0f8..5ba83313f4a 100644 --- a/packages/thirdweb/src/wallets/smart/smart-wallet.ts +++ b/packages/thirdweb/src/wallets/smart/smart-wallet.ts @@ -83,6 +83,20 @@ import type { SmartWalletOptions } from "./types.js"; * }); * ``` * + * ## Opt out of ERC-6492 signatures (use ERC-1271) + * + * This will deploy your smart account before signing any messages. This could slow down user logins but is useful if you need to verify signatures on-chain. + * + * ```ts + * import { smartWallet } from "thirdweb/wallets"; + * import { sepolia } from "thirdweb/chains"; + * + * const wallet = smartWallet({ + * chain: sepolia, + * eagerDeployment: true, // deploy the smart account before signing messages + * }); + * ``` + * * ## Using v0.7 Entrypoint * * Both v0.6 (default) and v0.7 ERC4337 Entrypoints are supported. To use the v0.7 Entrypoint, simply pass in a compatible account factory. diff --git a/packages/thirdweb/src/wallets/smart/types.ts b/packages/thirdweb/src/wallets/smart/types.ts index 6be9384a4f7..375bcf05356 100644 --- a/packages/thirdweb/src/wallets/smart/types.ts +++ b/packages/thirdweb/src/wallets/smart/types.ts @@ -21,6 +21,7 @@ export type SmartWalletOptions = Prettify< { chain: Chain; // TODO consider making default chain optional factoryAddress?: string; + eagerDeployment?: boolean; overrides?: { bundlerUrl?: string; accountAddress?: string;