From 852d741a160e7e09b71f48189d4953cd606cd210 Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Thu, 17 Apr 2025 14:34:29 +1000 Subject: [PATCH] feat: prefer `signTypedData` on owners for cbsw Co-Authored-By: Jainil Sutaria --- .changeset/brown-schools-care.md | 5 + .../toCoinbaseSmartAccount.test.ts | 100 ++++++++++++++++++ .../implementations/toCoinbaseSmartAccount.ts | 33 ++++-- 3 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 .changeset/brown-schools-care.md diff --git a/.changeset/brown-schools-care.md b/.changeset/brown-schools-care.md new file mode 100644 index 0000000000..4804c6d9f8 --- /dev/null +++ b/.changeset/brown-schools-care.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +**Account Abstraction:** Made `toCoinbaseSmartWallet` prefer `signTypedData` on owners. diff --git a/src/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.test.ts b/src/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.test.ts index e6ada3600a..1cbe59c6ac 100644 --- a/src/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.test.ts +++ b/src/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.test.ts @@ -415,6 +415,39 @@ describe('return value: sign', () => { expect(result).toBeTruthy() }) + + test('behavior: owner uses `sign` instead of `signTypedData`', async () => { + const owner = privateKeyToAccount(accounts[0].privateKey) + // @ts-expect-error + owner.signTypedData = undefined + + const account = await toCoinbaseSmartAccount({ + client, + owners: [owner], + nonce: 70n, + }) + + await writeContract(client, { + ...account.factory, + functionName: 'createAccount', + args: [[pad(owner.address)], 70n], + }) + await mine(client, { + blocks: 1, + }) + + const signature = await account.sign({ + hash: '0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68', + }) + + const result = await verifyHash(client, { + address: await account.getAddress(), + hash: '0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68', + signature, + }) + + expect(result).toBeTruthy() + }) }) describe('return value: signMessage', () => { @@ -466,6 +499,39 @@ describe('return value: signMessage', () => { expect(result).toBeTruthy() }) + + test('behavior: owner uses `sign` instead of `signTypedData`', async () => { + const owner = privateKeyToAccount(accounts[0].privateKey) + // @ts-expect-error + owner.signMessage = undefined + + const account = await toCoinbaseSmartAccount({ + client, + owners: [owner], + nonce: 70n, + }) + + await writeContract(client, { + ...account.factory, + functionName: 'createAccount', + args: [[pad(owner.address)], 70n], + }) + await mine(client, { + blocks: 1, + }) + + const signature = await account.signMessage({ + message: 'hello world', + }) + + const result = await verifyMessage(client, { + address: await account.getAddress(), + message: 'hello world', + signature, + }) + + expect(result).toBeTruthy() + }) }) describe('return value: signTypedData', () => { @@ -519,6 +585,40 @@ describe('return value: signTypedData', () => { }) expect(result).toBeTruthy() }) + + test('behavior: owner uses `sign` instead of `signTypedData`', async () => { + const owner = privateKeyToAccount(accounts[0].privateKey) + // @ts-expect-error + owner.signTypedData = undefined + + const account = await toCoinbaseSmartAccount({ + client, + owners: [owner], + nonce: 515151n, + }) + + await writeContract(client, { + ...account.factory, + functionName: 'createAccount', + args: [[pad(owner.address)], 515151n], + }) + await mine(client, { + blocks: 1, + }) + + const signature = await account.signTypedData({ + ...typedData.basic, + primaryType: 'Mail', + }) + + const result = await verifyTypedData(client, { + address: await account.getAddress(), + signature, + ...typedData.basic, + primaryType: 'Mail', + }) + expect(result).toBeTruthy() + }) }) describe('return value: signUserOperation', () => { diff --git a/src/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.ts b/src/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.ts index 22551ee3f7..296b903af4 100644 --- a/src/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.ts +++ b/src/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.ts @@ -177,14 +177,14 @@ export async function toCoinbaseSmartAccount( async sign(parameters) { const address = await this.getAddress() - const hash = toReplaySafeHash({ + const typedData = toReplaySafeTypedData({ address, chainId: client.chain!.id, hash: parameters.hash, }) if (owner.type === 'address') throw new Error('owner cannot sign') - const signature = await sign({ hash, owner }) + const signature = await signTypedData({ owner, typedData }) return wrapSignature({ ownerIndex, @@ -196,14 +196,14 @@ export async function toCoinbaseSmartAccount( const { message } = parameters const address = await this.getAddress() - const hash = toReplaySafeHash({ + const typedData = toReplaySafeTypedData({ address, chainId: client.chain!.id, hash: hashMessage(message), }) if (owner.type === 'address') throw new Error('owner cannot sign') - const signature = await sign({ hash, owner }) + const signature = await signTypedData({ owner, typedData }) return wrapSignature({ ownerIndex, @@ -216,7 +216,7 @@ export async function toCoinbaseSmartAccount( parameters as TypedDataDefinition const address = await this.getAddress() - const hash = toReplaySafeHash({ + const typedData = toReplaySafeTypedData({ address, chainId: client.chain!.id, hash: hashTypedData({ @@ -228,7 +228,7 @@ export async function toCoinbaseSmartAccount( }) if (owner.type === 'address') throw new Error('owner cannot sign') - const signature = await sign({ hash, owner }) + const signature = await signTypedData({ owner, typedData }) return wrapSignature({ ownerIndex, @@ -278,6 +278,21 @@ export async function toCoinbaseSmartAccount( // Utilities ///////////////////////////////////////////////////////////////////////////////////////////// +/** @internal */ +export async function signTypedData({ + typedData, + owner, +}: { + typedData: TypedDataDefinition + owner: OneOf +}) { + if (owner.type === 'local' && owner.signTypedData) + return owner.signTypedData(typedData) + + const hash = hashTypedData(typedData) + return sign({ hash, owner }) +} + /** @internal */ export async function sign({ hash, @@ -297,12 +312,12 @@ export async function sign({ } /** @internal */ -export function toReplaySafeHash({ +export function toReplaySafeTypedData({ address, chainId, hash, }: { address: Address; chainId: number; hash: Hash }) { - return hashTypedData({ + return { domain: { chainId, name: 'Coinbase Smart Wallet', @@ -321,7 +336,7 @@ export function toReplaySafeHash({ message: { hash, }, - }) + } as const } /** @internal */