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

Passkey support #808

Merged
merged 49 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
061321e
Add passkey support
DaniSomoza May 8, 2024
55e24a6
update createAddOwnerTx to receive passkey param
DaniSomoza May 9, 2024
6702b3d
fix init issue in relay-kit
DaniSomoza May 9, 2024
a74b809
fix issue in BaseContract
DaniSomoza May 9, 2024
33fd454
PasskeyArgType type as Uppercase
DaniSomoza Jun 4, 2024
d20dc53
remove Safe Proxy Factory mentions in the SafeWebAuthnSignerFactoryCo…
DaniSomoza Jun 4, 2024
dac782f
fix getSafeWebAuthnSignerFactoryContract params
DaniSomoza Jun 4, 2024
f1ef57e
remove left-hand side condition
DaniSomoza Jun 4, 2024
a61e70c
Add support for passkeys to 4337
DaniSomoza May 16, 2024
a547b98
verificationGasLimit adjustment
DaniSomoza May 30, 2024
b2ac8df
Added createSafeProvider util function
DaniSomoza Jun 4, 2024
eff8ab5
added createSafeProvider in relay-kit
DaniSomoza Jun 4, 2024
3d524ef
use createSafeProvider in SafeFactory
DaniSomoza Jun 4, 2024
3aa0fdd
feat(protocol-kit): Tests for passkey (#838)
tmjssz Jun 5, 2024
06cdedf
fix isTypedDataSigner to return false if it is passkey signer
DaniSomoza Jun 5, 2024
f3cf8ab
Merge branch 'passkey-support' into passkey-4337-support
DaniSomoza Jun 5, 2024
694488b
refactor createSafeProvider into a static async init method in SafePr…
DaniSomoza Jun 5, 2024
94cb8a0
flatten signingMethod ifs
DaniSomoza Jun 5, 2024
dd0dddc
removed getSafeWebAuthnSignerFactoryContract from Contract Manager
DaniSomoza Jun 5, 2024
625b668
Merge branch 'development' into passkey-support
DaniSomoza Jun 5, 2024
801f8f2
fix types in SafeFactory signer param
DaniSomoza Jun 5, 2024
7ba2e47
updated docs
DaniSomoza Jun 5, 2024
378c03e
Add FIXME comment to use the production deployment packages instead o…
DaniSomoza Jun 5, 2024
70e09af
Add Passkeys as an experimental feature only available on the Sepolia…
DaniSomoza Jun 6, 2024
4977cf6
Merge pull request #841 from safe-global/passkey-4337-support
dasanra Jun 6, 2024
2e284eb
Set alpha.0 version
dasanra Jun 6, 2024
7d58f60
add version compatibility check for passkeys
DaniSomoza Jun 10, 2024
200c6b2
Merge branch 'development' into passkey-support
dasanra Jun 10, 2024
a4a45de
Merge branch 'development' into passkey-support
dasanra Jun 12, 2024
6cbd3f6
feat(protocol-kit): Restrict passkeys tests according to safe version…
leonardotc Jun 12, 2024
d5f32ec
feat(relay-kit): add dummy signature as a passkey signature (#857)
DaniSomoza Jun 12, 2024
61a1280
feat(relay-kit): Tests for using passkey with 4337 (#846)
tmjssz Jun 17, 2024
102c3b6
Update passkey type (#859)
DaniSomoza Jun 18, 2024
9ff82fc
Set alpha.1 version
dasanra Jun 19, 2024
a51e837
feat(protocol-kit): Tests for swap and remove passkey owners (#861)
leonardotc Jun 20, 2024
3dd6595
[Passkeys] Detect Shared Signer owner (#875)
DaniSomoza Jul 5, 2024
1b91381
Merge branch 'development' into passkey-support
dasanra Jul 10, 2024
64cd846
Merge branch 'development' into passkey-support
dasanra Jul 10, 2024
8fbbded
Fix `safe-kit` reconnections
dasanra Jul 10, 2024
1ba2989
Prepare passkeys release (#937)
DaniSomoza Aug 12, 2024
b875321
Merge branch 'development' into passkey-support
dasanra Aug 12, 2024
eb1ce1a
fix(protocol-kit): build not finishing because of breaking change in …
dasanra Aug 12, 2024
36ef47f
Set alpha.2 version
dasanra Aug 12, 2024
13c8ef5
add comment to use external util
dasanra Aug 12, 2024
c31b351
Merge branch 'development' into passkey-support
dasanra Aug 14, 2024
d40e489
use contract addresses from safe-modules-deployments (#950)
DaniSomoza Aug 21, 2024
2081134
Merge branch 'development' into passkey-support
DaniSomoza Aug 22, 2024
d3da1e0
chore: remove bytecode from webauthn ABIs
dasanra Aug 22, 2024
1c22d9c
update passkeys contract names from v1_4_1 to v0.2.1 (#953)
DaniSomoza Aug 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/account-abstraction-kit/src/AccountAbstraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ class AccountAbstraction {
}

#initializeProtocolKit = async () => {
const safeProvider = new SafeProvider({ provider: this.#provider, signer: this.#signer })
const safeProvider = new SafeProvider({
provider: this.#provider,
signer: this.#signer as string
Copy link
Member

Choose a reason for hiding this comment

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

Can we avoid the cast ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done! I created a createSafeProvider util function to create a proper SafeProvider object based on the signer provided by the user.

I added this in the other PR (passkey-4337-support) to be able to use it also in the relay-kit. With this change the protocol-kit, the relay-kit and the account-abstraction-kit is compatible with passkeys.

})
const signer = await safeProvider.getSignerAddress()

if (!signer) {
Expand Down
159 changes: 149 additions & 10 deletions packages/protocol-kit/src/Safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import ModuleManager from './managers/moduleManager'
import OwnerManager from './managers/ownerManager'
import {
AddOwnerTxParams,
AddPasskeyOwnerTxParams,
Copy link
Member

Choose a reason for hiding this comment

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

Why two separate types? Could we use an Union type instead?

Copy link
Contributor Author

@DaniSomoza DaniSomoza Jun 4, 2024

Choose a reason for hiding this comment

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

I added two separate types to create guards:

export function isAddOwnerTxParams(
  params: AddOwnerTxParams | AddPasskeyOwnerTxParams
): params is AddOwnerTxParams {
  return (params as AddOwnerTxParams).ownerAddress !== undefined
}

export function isAddPasskeyOwnerTxParams(
  params: AddOwnerTxParams | AddPasskeyOwnerTxParams
): params is AddPasskeyOwnerTxParams {
  return (params as AddPasskeyOwnerTxParams).passkey !== undefined
}

ConnectSafeConfig,
CreateTransactionProps,
PredictedSafeProps,
Expand All @@ -38,7 +39,8 @@ import {
SigningMethod,
SigningMethodType,
SwapOwnerTxParams,
SafeModulesPaginated
SafeModulesPaginated,
passkeyArgType
} from './types'
import {
EthSafeSignature,
Expand All @@ -54,24 +56,28 @@ import {
generatePreValidatedSignature,
generateSignature,
preimageSafeMessageHash,
preimageSafeTransactionHash
preimageSafeTransactionHash,
adjustVInSignature
} from './utils'
import EthSafeTransaction from './utils/transactions/SafeTransaction'
import { SafeTransactionOptionalProps } from './utils/transactions/types'
import {
encodeMultiSendData,
isAddPasskeyOwnerTxParams,
standardizeMetaTransactionData,
standardizeSafeTransactionData
} from './utils/transactions/utils'
import { isSafeConfigWithPredictedSafe } from './utils/types'
import {
getCompatibilityFallbackHandlerContract,
getMultiSendCallOnlyContract,
getProxyFactoryContract
getProxyFactoryContract,
getSafeWebAuthnSignerFactoryContract
} from './contracts/safeDeploymentContracts'
import SafeMessage from './utils/messages/SafeMessage'
import semverSatisfies from 'semver/functions/satisfies'
import SafeProvider from './SafeProvider'
import PasskeySigner from './utils/passkeys/PasskeySigner'

const EQ_OR_GT_1_4_1 = '>=1.4.1'
const EQ_OR_GT_1_3_0 = '>=1.3.0'
Expand Down Expand Up @@ -114,10 +120,38 @@ class Safe {
async #initializeProtocolKit(config: SafeConfig) {
const { provider, signer, isL1SafeSingleton, contractNetworks } = config

this.#safeProvider = new SafeProvider({
provider,
signer
})
const isPasskeySigner = signer && typeof signer !== 'string'
Copy link
Contributor

Choose a reason for hiding this comment

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

Disclaimer: The comment has nothing to do with this line but wasnt the protocol kit supposed to have a getPasskeyCreationOptions or something like that for the people to have a schema on how to create a "safe-compliant" passkey or something?


if (isPasskeySigner) {
const safeProvider = new SafeProvider({
provider
})
const chainId = await safeProvider.getChainId()
const customContracts = contractNetworks?.[chainId.toString()]

const safeWebAuthnSignerFactoryContract = await getSafeWebAuthnSignerFactoryContract({
Copy link
Member

Choose a reason for hiding this comment

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

This can be retrieved from the safeProvider right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yep, but I updated this code to use the createSafeProvider util function to wrap all the logic about to create the safeProvider object to reuse it in all packages.

safeProvider,
safeVersion: '1.4.1',
customContracts
})

const passkeySigner = await PasskeySigner.init(
signer,
safeWebAuthnSignerFactoryContract,
safeProvider.getExternalProvider()
)

this.#safeProvider = new SafeProvider({
provider,
signer: passkeySigner
})
} else {
this.#safeProvider = new SafeProvider({
provider,
signer
})
}

if (isSafeConfigWithPredictedSafe(config)) {
this.#predictedSafe = config.predictedSafe
this.#contractManager = await ContractManager.init(
Expand Down Expand Up @@ -421,13 +455,37 @@ class Safe {
return this.#moduleManager.isModuleEnabled(moduleAddress)
}

/**
* Checks if a specific address or passkey is an owner of the current Safe.
*
* @param owner - The owner address or a passkey object
* @returns TRUE if the account is an owner
*/
async isOwner(owner: string | passkeyArgType): Promise<boolean> {
const isOwnerAddress = typeof owner === 'string'
Copy link
Contributor

Choose a reason for hiding this comment

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

The this.#safeProvider has an util to detect if this is an address. Consider to just query it. I dont think it will match eip3770 though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this case we need to know if the owner param is a string or a passkeyArgType not an address.


if (isOwnerAddress) {
return this.#isOwnerAddress(owner)
}

// passkey flow
const webAuthnSignerFactoryContract = this.#contractManager.safeWebAuthnSignerFactoryContract
const provider = this.#safeProvider.getExternalProvider()

const passkeySigner = await PasskeySigner.init(owner, webAuthnSignerFactoryContract, provider)

const ownerAddress = await passkeySigner.getAddress()
Copy link
Contributor

Choose a reason for hiding this comment

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

I may be wrong but isnt all this chunk just the this.#safeProvider.getSignerAddress()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the this.#safeProvider might have been initialized with an EOA instead of a passkey. We need to ensure that the owner's address is created from the passkey provided by the user in the isOwner function parameter, not from the one in the this.#safeProvider.


return this.#isOwnerAddress(ownerAddress)
}

/**
* Checks if a specific address is an owner of the current Safe.
*
* @param ownerAddress - The account address
* @returns TRUE if the account is an owner
*/
async isOwner(ownerAddress: string): Promise<boolean> {
async #isOwnerAddress(ownerAddress: string): Promise<boolean> {
if (this.#predictedSafe?.safeAccountConfig.owners) {
return Promise.resolve(
this.#predictedSafe?.safeAccountConfig.owners.some((owner: string) =>
Expand Down Expand Up @@ -727,6 +785,27 @@ class Safe {
throw new Error('Transactions can only be signed by Safe owners')
}

// passkey flow
const isPasskeySigner = await this.#safeProvider.isPasskeySigner()
if (isPasskeySigner) {
const txHash = await this.getTransactionHash(transaction)
const signedHash = await this.#safeProvider.signMessage(txHash)
Copy link
Member

Choose a reason for hiding this comment

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

We usually do this.signHash(txHash) for signing tx hashes and will adjust the V and return the EthSafeSignature object

Copy link
Contributor Author

Choose a reason for hiding this comment

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

true! I updated this in the 4337 support PR:

    const isPasskeySigner = await this.#safeProvider.isPasskeySigner()

    if (isPasskeySigner) {
      const txHash = await this.getTransactionHash(transaction)

      signature = await this.signHash(txHash)


const signatureAdjusted = adjustVInSignature(
SigningMethod.ETH_SIGN,
signedHash,
txHash,
signerAddress
)

const signature = new EthSafeSignature(signerAddress, signatureAdjusted, true)

const signedSafeTransaction = await this.copyTransaction(transaction)
signedSafeTransaction.addSignature(signature)

return signedSafeTransaction
}

const safeVersion = await this.getContractVersion()
if (
signingMethod === SigningMethod.SAFE_SIGNATURE &&
Expand Down Expand Up @@ -810,7 +889,6 @@ class Safe {
throw new Error('Transaction hashes can only be approved by Safe owners')
}

// TODO: fix this
return this.#contractManager.safeContract.approveHash(hash, {
from: signerAddress,
...options
Expand Down Expand Up @@ -999,9 +1077,17 @@ class Safe {
* @throws "Threshold cannot exceed owner count"
*/
async createAddOwnerTx(
{ ownerAddress, threshold }: AddOwnerTxParams,
params: AddOwnerTxParams | AddPasskeyOwnerTxParams,
options?: SafeTransactionOptionalProps
): Promise<SafeTransaction> {
const isPasskey = isAddPasskeyOwnerTxParams(params)

if (isPasskey) {
return this.#createAddPasskeyOwnerTx(params, options)
}

const { ownerAddress, threshold } = params

const safeTransactionData = {
to: await this.getAddress(),
value: '0',
Expand All @@ -1014,6 +1100,59 @@ class Safe {
return safeTransaction
}

/**
* Returns the Safe transaction to add a passkey as owner and optionally change the threshold.
*
* @param params - The transaction params
* @param options - The transaction optional properties
* @returns The Safe transaction ready to be signed
* @throws "Invalid owner address provided"
* @throws "Address provided is already an owner"
* @throws "Threshold needs to be greater than 0"
* @throws "Threshold cannot exceed owner count"
*/
async #createAddPasskeyOwnerTx(
{ passkey, threshold }: AddPasskeyOwnerTxParams,
options?: SafeTransactionOptionalProps
): Promise<SafeTransaction> {
const webAuthnSignerFactoryContract = this.#contractManager.safeWebAuthnSignerFactoryContract
const provider = this.#safeProvider.getExternalProvider()

const passkeySigner = await PasskeySigner.init(passkey, webAuthnSignerFactoryContract, provider)

const ownerAddress = await passkeySigner.getAddress()
const isPasskeySignerDeployed = await this.#safeProvider.isContractDeployed(ownerAddress)

// The passkey Signer is a contract compliant with EIP-1271 standards, we need to check if it has been deployed.
if (isPasskeySignerDeployed) {
return this.createAddOwnerTx({ ownerAddress, threshold }, options)
}

// If it has not been deployed, we need to create a batch that includes both the Signer contract deployment and the addOwner transaction

// First transaction of the batch: The Deployment of the Signer
const createSignerTransaction = {
to: await passkeySigner.safeWebAuthnSignerFactoryContract.getAddress(),
value: '0',
data: passkeySigner.encodeCreateSigner()
}

// Second transaction of the batch: The AddOwner transaction
const addOwnerTransaction = {
to: await this.getAddress(),
value: '0',
data: await this.#ownerManager.encodeAddOwnerWithThresholdData(ownerAddress, threshold)
}

// transactions for the batch
const transactions = [createSignerTransaction, addOwnerTransaction]

return await this.createTransaction({
transactions,
options
})
}

/**
* Returns the Safe transaction to remove an owner and optionally change the threshold.
*
Expand Down
34 changes: 33 additions & 1 deletion packages/protocol-kit/src/SafeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from '@safe-global/protocol-kit/types'
import { SafeVersion } from '@safe-global/safe-core-sdk-types'
import SafeProvider from '@safe-global/protocol-kit/SafeProvider'
import PasskeySigner from './utils/passkeys/PasskeySigner'

class SafeFactory {
#contractNetworks?: ContractNetworksConfig
Expand Down Expand Up @@ -62,7 +63,38 @@ class SafeFactory {
}: SafeFactoryInitConfig) {
this.#provider = provider
this.#signer = signer
this.#safeProvider = new SafeProvider({ provider, signer })
const isPasskeySigner = signer && typeof signer !== 'string'
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider on extracting this to a local util since its reused.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done! I addressed this comment in this other PR. I created a util in called createSafeProvider to reuse all the logic behind this:

Captura de pantalla 2024-06-04 a las 19 20 29

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After your suggestion in this comment, I create an static async init method in the SafeProvider instead of the createSafeProvider


if (isPasskeySigner) {
const safeProvider = new SafeProvider({
provider
})
const chainId = await safeProvider.getChainId()
const customContracts = contractNetworks?.[chainId.toString()]

const safeWebAuthnSignerFactoryContract =
await safeProvider.getSafeWebAuthnSignerFactoryContract({
safeVersion: '1.4.1',
customContractAddress: customContracts?.safeWebAuthnSignerFactoryAddress,
customContractAbi: customContracts?.safeWebAuthnSignerFactoryAbi
})

const passkeySigner = await PasskeySigner.init(
signer,
safeWebAuthnSignerFactoryContract,
safeProvider.getExternalProvider()
)

this.#safeProvider = new SafeProvider({
provider,
signer: passkeySigner
})
} else {
this.#safeProvider = new SafeProvider({
provider,
signer
})
}
this.#safeVersion = safeVersion
this.#isL1SafeSingleton = isL1SafeSingleton
this.#contractNetworks = contractNetworks
Expand Down
Loading
Loading