Skip to content

Commit

Permalink
Rework Safe Signer Launchpad (#377)
Browse files Browse the repository at this point in the history
This PR does a rework of some of the implementation details of the
`SafeSignerLaunchpad` contract in light of some observations from the
previous PR #376.

Namely, this changes the initialization process to work in a slightly
different way:
1. Set the target singleton to a special slot when the entry point
executes the `initCode` for the account. Safe `setup` also happens at
this point, meaning that any `DELEGATECALL` to the Safe singleton should
work and be valid.
2. Signature verification checks that the account is an owner. This has
the side-effect that you can initialize an account with multiple custom
ECDSA owners and use any of them to sign the first user operation.
3. Promote the Safe to the singleton that was previously in storage.

The main difference with the previous flow is that we no longer have two
separate `setup` initializers that we `DELEGATECALL` to. Additionally,
we added checks that prevent double initialization as well as reentrency
issues in the execution function.

In addition, this also opens up a pretty clear path for supporting
multiple owners with the launchpad as the account has already undergone
"regular" Safe setup. This is relevant for #372.

Unit tests in order to reach 100% coverage will be introduced in a
follow up.
  • Loading branch information
nlordell authored May 10, 2024
1 parent f5b9f22 commit b143b6f
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 355 deletions.
2 changes: 1 addition & 1 deletion examples/4337-passkeys/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const APP_CHAIN_SHORTNAME = 'sep'
the production deployment packages, thus we need to hardcode their addresses here.
Deployment commit: https://github.com/safe-global/safe-modules/commit/3853f34f31837e0a0aee47a4452564278f8c62ba
*/
const SAFE_SIGNER_LAUNCHPAD_ADDRESS = '0xf9E93EfBF37B588f9c79d509c48b87A26c3DfEB9'
const SAFE_SIGNER_LAUNCHPAD_ADDRESS = '0xEC909139De44e3d1403602B06cc0BB1ab3C143f2'

const SAFE_4337_MODULE_ADDRESS = '0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226'

Expand Down
97 changes: 45 additions & 52 deletions examples/4337-passkeys/src/logic/safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,32 +52,18 @@ type SafeInitializer = {
fallbackHandler: string
}

function getInitHash(safeInitializer: SafeInitializer, chainId: ethers.BigNumberish): string {
return ethers.TypedDataEncoder.hash(
{ verifyingContract: SAFE_SIGNER_LAUNCHPAD_ADDRESS, chainId },
{
SafeInit: [
{ type: 'address', name: 'singleton' },
{ type: 'address', name: 'signerFactory' },
{ type: 'uint256', name: 'signerX' },
{ type: 'uint256', name: 'signerY' },
{ type: 'uint176', name: 'signerVerifiers' },
{ type: 'address', name: 'setupTo' },
{ type: 'bytes', name: 'setupData' },
{ type: 'address', name: 'fallbackHandler' },
],
},
safeInitializer,
)
}

function getLaunchpadInitializer(safeInitHash: string, optionalCallAddress = ethers.ZeroAddress, optionalCalldata = '0x'): string {
function getLaunchpadInitializer(initializer: SafeInitializer): string {
const safeSignerLaunchpadInterface = new ethers.Interface(SafeSignerLaunchpadAbi) as unknown as SafeSignerLaunchpad['interface']

const launchpadInitializer = safeSignerLaunchpadInterface.encodeFunctionData('preValidationSetup', [
safeInitHash,
optionalCallAddress,
optionalCalldata,
const launchpadInitializer = safeSignerLaunchpadInterface.encodeFunctionData('setup', [
initializer.singleton,
initializer.signerFactory,
initializer.signerX,
initializer.signerY,
initializer.signerVerifiers,
initializer.setupTo,
initializer.setupData,
initializer.fallbackHandler,
])

return launchpadInitializer
Expand Down Expand Up @@ -129,28 +115,51 @@ function encodeSafeModuleSetupCall(modules: string[]): string {

/**
* Encodes the necessary data for initializing a Safe contract and performing a user operation.
* @param initializer - The SafeInitializer object containing the initialization parameters.
* @param encodedUserOp - The encoded user operation data.
* @param setup - The SafeInitializer object containing the initialization parameters.
* @param to The address of the recipient of the operation.
* @param value The amount of value to be transferred in the operation.
* @param data The data payload of the operation.
* @param operation The type of operation (0 for CALL, 1 for DELEGATECALL).
* @returns The encoded data for initializing the Safe contract and performing the user operation.
*/
function getLaunchpadInitializeThenUserOpData(initializer: SafeInitializer, encodedUserOp: string): string {
function getPromoteAccountAndExecuteUserOpData(
initializer: SafeInitializer,
to: string,
value: ethers.BigNumberish,
data: string,
operation: 0 | 1,
): string {
const safeSignerLaunchpadInterface = new ethers.Interface(SafeSignerLaunchpadAbi) as unknown as SafeSignerLaunchpad['interface']

const initializeThenUserOpData = safeSignerLaunchpadInterface.encodeFunctionData('initializeThenUserOp', [
initializer.singleton,
const initializeThenUserOpData = safeSignerLaunchpadInterface.encodeFunctionData('promoteAccountAndExecuteUserOp', [
initializer.signerFactory,
initializer.signerX,
initializer.signerY,
initializer.signerVerifiers,
initializer.setupTo,
initializer.setupData,
initializer.fallbackHandler,
encodedUserOp,
to,
value,
data,
operation,
])

return initializeThenUserOpData
}

/**
* Encodes the user operation data for validating a user operation.
* @param userOp The packed user operation to be validated.
* @param userOpHash The hash of the user operation.
* @param missingAccountFunds The amount of missing account funds.
* @returns The encoded data for validating the user operation.
*/
function getValidateUserOpData(userOp: PackedUserOperation, userOpHash: string, missingAccountFunds: ethers.BigNumberish): string {
const safe4337ModuleInterface = new ethers.Interface(Safe4337ModuleAbi) as unknown as Safe4337Module['interface']

const validateUserOpData = safe4337ModuleInterface.encodeFunctionData('validateUserOp', [userOp, userOpHash, missingAccountFunds])

return validateUserOpData
}

/**
* Encodes the parameters of a user operation for execution on Safe4337Module.
* @param to The address of the recipient of the operation.
Expand All @@ -167,30 +176,14 @@ function getExecuteUserOpData(to: string, value: ethers.BigNumberish, data: stri
return executeUserOpData
}

/**
* Encodes the user operation data for validating a user operation.
* @param userOp The packed user operation to be validated.
* @param userOpHash The hash of the user operation.
* @param missingAccountFunds The amount of missing account funds.
* @returns The encoded data for validating the user operation.
*/
function getValidateUserOpData(userOp: PackedUserOperation, userOpHash: string, missingAccountFunds: ethers.BigNumberish): string {
const safe4337ModuleInterface = new ethers.Interface(Safe4337ModuleAbi) as unknown as Safe4337Module['interface']

const validateUserOpData = safe4337ModuleInterface.encodeFunctionData('validateUserOp', [userOp, userOpHash, missingAccountFunds])

return validateUserOpData
}
export type { SafeInitializer }

export {
getExecuteUserOpData,
getLaunchpadInitializeThenUserOpData,
getLaunchpadInitializer,
getPromoteAccountAndExecuteUserOpData,
getSignerAddressFromPubkeyCoords,
getSafeDeploymentData,
getSafeAddress,
getValidateUserOpData,
getInitHash,
getLaunchpadInitializer,
getExecuteUserOpData,
encodeSafeModuleSetupCall,
}
11 changes: 3 additions & 8 deletions examples/4337-passkeys/src/logic/userOp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import { abi as EntryPointAbi } from '@account-abstraction/contracts/artifacts/E
import { IEntryPoint } from '@safe-global/safe-4337/dist/typechain-types'
import {
SafeInitializer,
getPromoteAccountAndExecuteUserOpData,
getExecuteUserOpData,
getInitHash,
getLaunchpadInitializeThenUserOpData,
getLaunchpadInitializer,
getSafeAddress,
getSafeDeploymentData,
Expand Down Expand Up @@ -186,8 +185,7 @@ function prepareUserOperationWithInitialisation(
afterInitializationOpCall?: UserOpCall,
saltNonce = ethers.ZeroHash,
): UnsignedPackedUserOperation {
const initHash = getInitHash(initializer, APP_CHAIN_ID)
const launchpadInitializer = getLaunchpadInitializer(initHash)
const launchpadInitializer = getLaunchpadInitializer(initializer)
const predictedSafeAddress = getSafeAddress(launchpadInitializer, SAFE_PROXY_FACTORY_ADDRESS, SAFE_SIGNER_LAUNCHPAD_ADDRESS, saltNonce)
const safeDeploymentData = getSafeDeploymentData(SAFE_SIGNER_LAUNCHPAD_ADDRESS, launchpadInitializer, saltNonce)
const userOpCall = afterInitializationOpCall ?? {
Expand All @@ -201,10 +199,7 @@ function prepareUserOperationWithInitialisation(
sender: predictedSafeAddress,
nonce: ethers.toBeHex(0),
initCode: getUserOpInitCode(proxyFactoryAddress, safeDeploymentData),
callData: getLaunchpadInitializeThenUserOpData(
initializer,
getExecuteUserOpData(userOpCall.to, userOpCall.value, userOpCall.data, userOpCall.operation),
),
callData: getPromoteAccountAndExecuteUserOpData(initializer, userOpCall.to, userOpCall.value, userOpCall.data, userOpCall.operation),
...packGasParameters({
callGasLimit: ethers.toBeHex(2000000),
verificationGasLimit: ethers.toBeHex(2000000),
Expand Down
6 changes: 2 additions & 4 deletions examples/4337-passkeys/src/routes/DeploySafe.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useMemo, useState } from 'react'
import { LoaderFunction, Navigate, redirect, useLoaderData } from 'react-router-dom'
import { encodeSafeModuleSetupCall, getInitHash, getLaunchpadInitializer, getSafeAddress } from '../logic/safe'
import { encodeSafeModuleSetupCall, getLaunchpadInitializer, getSafeAddress } from '../logic/safe'
import type { SafeInitializer } from '../logic/safe'
import {
SAFE_4337_MODULE_ADDRESS,
Expand All @@ -9,7 +9,6 @@ import {
SAFE_SINGLETON_ADDRESS,
WEBAUTHN_SIGNER_FACTORY_ADDRESS,
P256_VERIFIER_ADDRESS,
APP_CHAIN_ID,
} from '../config'
import { getPasskeyFromLocalStorage, PasskeyLocalStorageFormat } from '../logic/passkeys'
import {
Expand Down Expand Up @@ -46,8 +45,7 @@ const loader: LoaderFunction = async () => {
setupTo: SAFE_MODULE_SETUP_ADDRESS,
setupData: encodeSafeModuleSetupCall([SAFE_4337_MODULE_ADDRESS]),
}
const initHash = getInitHash(initializer, APP_CHAIN_ID)
const launchpadInitializer = getLaunchpadInitializer(initHash)
const launchpadInitializer = getLaunchpadInitializer(initializer)
const safeAddress = getSafeAddress(launchpadInitializer)

return { passkey, safeAddress }
Expand Down
6 changes: 2 additions & 4 deletions examples/4337-passkeys/src/routes/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Navigate, redirect, useLoaderData } from 'react-router-dom'
import { getPasskeyFromLocalStorage, PasskeyLocalStorageFormat } from '../logic/passkeys.ts'
import { encodeSafeModuleSetupCall, getInitHash, getLaunchpadInitializer, getSafeAddress, type SafeInitializer } from '../logic/safe.ts'
import { encodeSafeModuleSetupCall, getLaunchpadInitializer, getSafeAddress, type SafeInitializer } from '../logic/safe.ts'
import {
APP_CHAIN_ID,
P256_VERIFIER_ADDRESS,
SAFE_4337_MODULE_ADDRESS,
SAFE_MODULE_SETUP_ADDRESS,
Expand Down Expand Up @@ -36,8 +35,7 @@ async function loader(): Promise<Response | LoaderData> {
setupTo: SAFE_MODULE_SETUP_ADDRESS,
setupData: encodeSafeModuleSetupCall([SAFE_4337_MODULE_ADDRESS]),
}
const initHash = getInitHash(initializer, APP_CHAIN_ID)
const launchpadInitializer = getLaunchpadInitializer(initHash)
const launchpadInitializer = getLaunchpadInitializer(initializer)
const safeAddress = getSafeAddress(launchpadInitializer)

return { passkey, safeAddress }
Expand Down
Loading

0 comments on commit b143b6f

Please sign in to comment.