Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Coinbase Smart Wallet #245

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

cstoneham
Copy link

Adds support for Coinbase Smart Wallet.

Working

  • Sending transactions

Not working

  • Signing Typed Data
  • Signing Messages

References

https://github.com/wilsoncusack/scw-tx
https://github.com/coinbase/smart-wallet

Copy link

changeset-bot bot commented Jul 9, 2024

⚠️ No Changeset found

Latest commit: 2b0daa0

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@cstoneham cstoneham changed the title [WIP] Support Coinbase Smart Wallet Support Coinbase Smart Wallet Jul 10, 2024
@cstoneham
Copy link
Author

FYI this is the test suite that I have in our Rainmaker repo, I've just pulled it out as a snippet because I wasn't sure where this belongs in your codebase. Note that I have some test PKs that I pulled out (they'll need to be put back in), encoded data tests will have to change too.

import {
  CoinbaseSmartAccount,
  privateKeyToCoinbaseSmartAccount,
} from "common/lib/coinbase/privateKeyToCoinbaseSmartAccount";
import getPublicClient from "common/utils/getPublicClient";
import { ENTRYPOINT_ADDRESS_V06, isSmartAccountDeployed } from "permissionless";
import {
  ENTRYPOINT_ADDRESS_V06_TYPE,
  UserOperation,
} from "permissionless/types";
import { Address, Hex, PublicClient, TypedDataDefinition } from "viem";
import { privateKeyToAccount } from "viem/accounts";

describe("privateKeyToCoinbaseSmartAccount", () => {
  let publicClient: PublicClient;
  let deployedAccount: CoinbaseSmartAccount<ENTRYPOINT_ADDRESS_V06_TYPE>;
  let undeployedAccount: CoinbaseSmartAccount<ENTRYPOINT_ADDRESS_V06_TYPE>;

  beforeAll(async () => {
    publicClient = getPublicClient({
      chain: "base",
      type: "production",
    });

    const deployedPK =
      "0x";
    deployedAccount = await privateKeyToCoinbaseSmartAccount(publicClient, {
      privateKey: deployedPK,
      initialOwners: [privateKeyToAccount(deployedPK).address],
      factoryAddress: COINBASE_SMART_WALLET_FACTORY_ADDRESS,
      entryPoint: ENTRYPOINT_ADDRESS_V06,
      ownerIndex: 0n,
    });

    const undeployedPK =
      "0x";
    undeployedAccount = await privateKeyToCoinbaseSmartAccount(publicClient, {
      privateKey: undeployedPK,
      initialOwners: [privateKeyToAccount(undeployedPK).address],
      factoryAddress: COINBASE_SMART_WALLET_FACTORY_ADDRESS,
      entryPoint: ENTRYPOINT_ADDRESS_V06,
      ownerIndex: 1n,
    });
  });

  it("account addresses are different by pk", async () => {
    expect(deployedAccount.address).not.toBe(undeployedAccount.address);
  });

  it("account address - deployed", async () => {
    // verify deployment status
    const smartAccountDeployed = await isSmartAccountDeployed(
      publicClient,
      deployedAccount.address,
    );

    expect(smartAccountDeployed).toBe(true);
  });

  it("account address - undeployed", async () => {
    // verify deployment status
    const smartAccountDeployed = await isSmartAccountDeployed(
      publicClient,
      undeployedAccount.address,
    );

    expect(smartAccountDeployed).toBe(false);
  });

  it("signMessage - deployed", async () => {
    const message = "hello world";

    // sign the message
    const signedMessage = await deployedAccount.signMessage({
      message,
    });

    const result = await publicClient.verifyMessage({
      address: deployedAccount.address,
      message,
      signature: signedMessage,
    });

    expect(result).toBe(true);
  });

  it("signMessage - undeployed", async () => {
    const message = "hello world";

    // sign the message
    const signedMessage = await undeployedAccount.signMessage({
      message,
    });

    const result = await publicClient.verifyMessage({
      address: undeployedAccount.address,
      message,
      signature: signedMessage,
    });

    expect(result).toBe(true);
  });

  it("signTypedData - deployed", async () => {
    const signature = await deployedAccount.signTypedData(TYPED_DATA);

    const result = await publicClient.verifyTypedData({
      address: deployedAccount.address,
      domain: TYPED_DATA.domain,
      types: TYPED_DATA.types,
      primaryType: TYPED_DATA.primaryType,
      message: TYPED_DATA.message,
      signature,
    });

    expect(result).toBe(true);
  });

  it("signTypedData - undeployed", async () => {
    const signature = await undeployedAccount.signTypedData(TYPED_DATA);

    const result = await publicClient.verifyTypedData({
      address: undeployedAccount.address,
      domain: TYPED_DATA.domain,
      types: TYPED_DATA.types,
      primaryType: TYPED_DATA.primaryType,
      message: TYPED_DATA.message,
      signature,
    });

    expect(result).toBe(true);
  });

  it("getNonce - deployed", async () => {
    const nonce = await deployedAccount.getNonce();
    expect(nonce).toBeGreaterThanOrEqual(0);
  });

  it("getNonce - undeployed", async () => {
    const nonce = await undeployedAccount.getNonce();
    expect(nonce).eq(0n);
  });

  it("getInitCode - deployed", async () => {
    const initCode = await deployedAccount.getInitCode();
    expect(initCode).toBe("0x");
  });

  it("getInitCode - undeployed", async () => {
    const initCode = await undeployedAccount.getInitCode();
    expect(initCode).toBe(
      "0x4ada7b58d006aca9670daba300c0ee3f2ed1c1b03ffba36f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000D2c94f2c39de59ba7879d196d1fFc4c2D9ff7Fa",
    );
  });

  it("getFactory - deployed", async () => {
    const factory = await deployedAccount.getFactory();
    expect(factory).toBeUndefined();
  });

  it("getFactory - undeployed", async () => {
    const factory = await undeployedAccount.getFactory();
    expect(factory).toBe(RAINMAKER_WALLET_FACTORY_ADDRESS);
  });

  it("getFactoryData - deployed", async () => {
    const factoryData = await deployedAccount.getFactoryData();
    expect(factoryData).toBeUndefined();
  });

  it("getFactoryData - undeployed", async () => {
    const factoryData = await undeployedAccount.getFactoryData();
    expect(factoryData).toBe(
      "0x3ffba36f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000D2c94f2c39de59ba7879d196d1fFc4c2D9ff7Fa",
    );
  });

  it("encodeCallData", async () => {
    const sendCall = {
      to: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" as Address,
      value: 1000000000000n,
      data: "0x" as Hex,
    };

    const encodedSingle = await undeployedAccount.encodeCallData(sendCall);

    expect(encodedSingle).toBe(
      "0xb61d27f6000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000e8d4a5100000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000",
    );

    const encodedMulti = await undeployedAccount.encodeCallData([
      sendCall,
      sendCall,
    ]);

    expect(encodedMulti).toBe(
      "0x34fcd5be00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000e8d4a5100000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000e8d4a5100000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000",
    );
  });

  it("getDummySignature", async () => {
    const dummySignature = await undeployedAccount.getDummySignature(
      {} as UserOperation<"v0.6">,
    );

    expect(dummySignature).toBe(
      "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000041000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
    );
  });
});

const TYPED_DATA: TypedDataDefinition = {
  domain: {
    name: "MyDapp",
    version: "1.0",
    chainId: 1,
    verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
  },
  types: {
    Person: [
      { name: "name", type: "string" },
      { name: "wallet", type: "address" },
    ],
    Mail: [
      { name: "from", type: "Person" },
      { name: "to", type: "Person" },
      { name: "contents", type: "string" },
    ],
  },
  primaryType: "Mail",
  message: {
    from: {
      name: "Alice",
      wallet: "0x1234567890123456789012345678901234567890",
    },
    to: {
      name: "Bob",
      wallet: "0x9876543210987654321098765432109876543210",
    },
    contents: "Hello, Bob!",
  },
};

@WardenJakx
Copy link

Legend

I've been waiting for this 🙏

plz merge asap

@dqian
Copy link

dqian commented Jul 10, 2024

ty

@carakessler
Copy link

whoa thank you @cstoneham !!!!! merge merge merge

export const COINBASE_SMART_WALLET_FACTORY_ADDRESS =
"0x0BA5ED0c6AA8c49038F819E587E2633c4A9F428a"

export const ERC1271InputGeneratorByteCode =

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we've actually moved away from using this in favor of just manually computing the replaySafeHash. Will try to share code

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah would love to see how you guys do that, thanks

Copy link

@jxom jxom Jul 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you go:

import { hashTypedData, type Address, type Hash } from 'viem'

function replaySafeHash({
  address,
  chainId,
  hash,
}: { address: Address; chainId: number; hash: Hash }) {
  return hashTypedData({
    domain: {
      chainId,
      name: 'Coinbase Smart Wallet',
      verifyingContract: address,
      version: '1',
    },
    types: {
      CoinbaseSmartWalletMessage: [
        {
          name: 'hash',
          type: 'bytes32',
        },
      ],
    },
    primaryType: 'CoinbaseSmartWalletMessage',
    message: {
      hash,
    },
  })
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, works.

@cstoneham
Copy link
Author

cstoneham commented Jul 18, 2024

@wilsoncusack I notice that I'll sometimes get: Expected bytes32, got bytes31.5. depending on the message that I'm signing or the private key that I'm using, getting thrown from here:

export function buildSignatureWrapperForEOA({
  signature,
  ownerIndex,
}: {
  signature: SignReturnType;
  ownerIndex: bigint;
}): Hex {
  if (signature.v === undefined) {
    throw new Error("[buildSignatureWrapperForEOA] Invalid signature");
  }

  const signatureData = encodePacked(
    ["bytes32", "bytes32", "uint8"],
    [signature.r, signature.s, parseInt(signature.v.toString())],
  );
  return encodeAbiParameters(
    [SignatureWrapperStruct],
    [
      {
        ownerIndex,
        signatureData,
      },
    ],
  );
}

have you seen this before? I assume there's some hex padding or something that needs to be done.

more specifically the signature.r and/or signature.s will be 65 chars vs 66 which is why this is getting thrown.

@jxom
Copy link

jxom commented Jul 18, 2024

@cstoneham – that may be a small bug (or inconsistency rather) in Viem. Can you try [email protected] and tell me if that works? Can release shortly!

@cstoneham
Copy link
Author

cstoneham commented Jul 18, 2024

[email protected]

Confirmed that this version fixes the issue, thanks @jxom.

What was the problem?

@jxom
Copy link

jxom commented Jul 18, 2024

Released! We needed to pad the signature properties to 32 bytes to conform with bytes32 encoding (31 bytes was still valid, but obviously doesn't fit well with ABI encoding APIs as you experienced).

@cstoneham
Copy link
Author

cstoneham commented Jul 19, 2024

thanks @jxom, so fast! when do you think the next patch version might be released (2.17.6)?

@jxom
Copy link

jxom commented Jul 20, 2024

Released on 2.17.7

@cstoneham
Copy link
Author

thanks for your help @jxom @wilsoncusack, think this is ready for another review.

@wilsoncusack
Copy link

@kristofgazso thoughts on this?

@plusminushalf
Copy link
Contributor

Thank you @cstoneham let me check this today. I will be updating this branch and merging this with #265 so that it's part of the 0.2 release.

@plusminushalf
Copy link
Contributor

Hey I had a few question:

  1. What is the difference between ownerIndex & Index?
  2. If there are multiple initialOwners will the privateKey used here still be able to sign and send user operation?

@plusminushalf
Copy link
Contributor

Also in v0.2 we are using Viem's account-abstraction methods and viem already had coin base implementation at toCoinbaseSmartAccount I am not sure permissionless also need to host the implementation again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants