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

feat: issuePermissions flow with safe7579 user operation builder #578

Merged
merged 8 commits into from
Jun 17, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,19 @@ import {
getSafe7579InitialValidators,
signerToSafe7579SmartAccount
} from '@/utils/safe7579AccountUtils/signerToSafe7579SmartAccount'
import { Address, Hex, concatHex, encodeFunctionData, keccak256, zeroAddress } from 'viem'
import {
Address,
Hex,
concatHex,
encodeFunctionData,
encodePacked,
keccak256,
zeroAddress
} from 'viem'
import { signMessage } from 'viem/accounts'
import {
PERMISSION_VALIDATOR_ADDRESS,
SAFE7579_USER_OPERATION_BUILDER_ADDRESS,
SECP256K1_SIGNATURE_VALIDATOR_ADDRESS
} from '@/utils/permissionValidatorUtils/constants'
import {
Expand All @@ -24,6 +33,8 @@ import {
} from '@/utils/permissionValidatorUtils'
import { setupSafeAbi } from '@/utils/safe7579AccountUtils/abis/Launchpad'
import { Execution } from '@/utils/safe7579AccountUtils/userop'
import { isModuleInstalledAbi } from '@/utils/safe7579AccountUtils/abis/Account'
import { ethers } from 'ethers'

export class SafeSmartAccountLib extends SmartAccountLib {
async getClientConfig(): Promise<SmartAccountClientConfig<EntryPoint>> {
Expand All @@ -46,20 +57,10 @@ export class SafeSmartAccountLib extends SmartAccountLib {
}

async sendTransaction({ to, value, data }: Execution) {
if (!this.client || !this.client.account) {
if (!this.client?.account) {
throw new Error('Client not initialized')
}
const accountDeployed = await isSmartAccountDeployed(
this.publicClient,
this.client.account.address
)
if (!accountDeployed) {
const setUpAndExecuteUserOpHash = await this.setupSafe7579AndExecute({ to, value, data })
const userOpReceipt = await this.bundlerClient.waitForUserOperationReceipt({
hash: setUpAndExecuteUserOpHash
})
return userOpReceipt.receipt.transactionHash
}
await this.setupSafe7579({ to, value, data })

const txResult = await this.client.sendTransaction({
to,
Expand All @@ -73,16 +74,10 @@ export class SafeSmartAccountLib extends SmartAccountLib {
}

async sendBatchTransaction(calls: Execution[]) {
if (!this.client || !this.client.account) {
if (!this.client?.account) {
throw new Error('Client not initialized')
}
const accountDeployed = await isSmartAccountDeployed(
this.publicClient,
this.client.account.address
)
if (!accountDeployed) {
return await this.setupSafe7579AndExecute(calls)
}
await this.setupSafe7579(calls)

const userOp = await this.client.prepareUserOperationRequest({
userOperation: {
Expand All @@ -100,69 +95,149 @@ export class SafeSmartAccountLib extends SmartAccountLib {
return userOpHash
}

async setupSafe7579AndExecute(calls: Execution | Execution[]) {
if (!this.client || !this.client.account) {
async issuePermissionContext(
targetAddress: Address,
approvedPermissions: any
): Promise<PermissionContext> {
if (!this.client?.account) {
throw new Error('Client not initialized')
}

const initialValidators = getSafe7579InitialValidators()
const initData = getSafe7579InitData(this.signer.address, initialValidators, calls)
const setUpSafe7579Calldata = encodeFunctionData({
abi: setupSafeAbi,
functionName: 'setupSafe',
args: [initData]
// setUpSafe account
await this.setupSafe7579({
data: '0x',
to: zeroAddress,
value: BigInt(0)
})
const setUpUserOp = await this.client.prepareUserOperationRequest({
userOperation: {
callData: setUpSafe7579Calldata
},
account: this.client.account
})
const newSignature = await this.client.account.signUserOperation(setUpUserOp)
// check permissionvalidator module is installed or not
const isInstalled = await this.isPermissionValidatorModuleInstalled()

setUpUserOp.signature = newSignature
if (!isInstalled) {
throw new Error(
'isPermissionValidatorModuleInstalled == false \n Should not have happen, need to debug initCode to check safe setUp process'
)
}

const setUpAndExecuteUserOpHash = await this.bundlerClient.sendUserOperation({
userOperation: setUpUserOp
})
const { permissionsContext, permissions, permittedScopeData, permittedScopeSignature } =
await this.getAllowedPermissionsAndData(targetAddress)

return {
accountType: 'Safe7579',
accountAddress: this.client.account.address,
permissionsContext: permissionsContext,
userOperationBuilder: SAFE7579_USER_OPERATION_BUILDER_ADDRESS,
//below are temporary additional values
permissionValidatorAddress: PERMISSION_VALIDATOR_ADDRESS,
permissions: permissions,
permittedScopeData: permittedScopeData,
permittedScopeSignature: permittedScopeSignature
}
}

return setUpAndExecuteUserOpHash
private async setupSafe7579(calls: Execution | Execution[]) {
if (!this.client?.account) {
throw new Error('Client not initialized')
}
const accountDeployed = await isSmartAccountDeployed(
this.publicClient,
this.client.account.address
)
if (!accountDeployed) {
const initialValidators = getSafe7579InitialValidators()
const initData = getSafe7579InitData(this.signer.address, initialValidators, calls)
const setUpSafe7579Calldata = encodeFunctionData({
abi: setupSafeAbi,
functionName: 'setupSafe',
args: [initData]
})
const setUpUserOp = await this.client.prepareUserOperationRequest({
userOperation: {
callData: setUpSafe7579Calldata
},
account: this.client.account
})
const newSignature = await this.client.account.signUserOperation(setUpUserOp)

setUpUserOp.signature = newSignature

const setUpAndExecuteUserOpHash = await this.bundlerClient.sendUserOperation({
userOperation: setUpUserOp
})
const userOpReceipt = await this.bundlerClient.waitForUserOperationReceipt({
hash: setUpAndExecuteUserOpHash
})
console.log({ setupSafetxHash: userOpReceipt.receipt.transactionHash })
}
}

async issuePermissionContext(
targetAddress: Address,
approvedPermissions: any
): Promise<PermissionContext> {
if (!this.client || !this.client.account) {
private async isPermissionValidatorModuleInstalled() {
if (!this.client?.account) {
throw new Error('Client not initialized')
}
return await this.publicClient.readContract({
address: this.client.account.address,
abi: isModuleInstalledAbi,
functionName: 'isModuleInstalled',
args: [
BigInt(1), // ModuleType
PERMISSION_VALIDATOR_ADDRESS, // Module Address
'0x' // Additional Context
]
})
}

private async getAllowedPermissionsAndData(signer: Address) {
// if installed then based on the approvedPermissions build the PermissionsContext value
// permissionsContext = [PERMISSION_VALIDATOR_ADDRESS][ENCODED_PERMISSION_SCOPE & SIGNATURE_DATA]

// this permission have dummy policy set to zeroAddress for now,
// bc current version of PermissionValidator_v1 module don't consider checking policy
const permissions: SingleSignerPermission[] = [
{
validUntil: 0,
validAfter: 0,
signatureValidationAlgorithm: SECP256K1_SIGNATURE_VALIDATOR_ADDRESS,
signer: targetAddress,
signer: signer,
policy: zeroAddress,
policyData: '0x'
}
]

console.log(`computing permission scope data...`)
const permittedScopeData = getPermissionScopeData(permissions, this.chain)
console.log(`user account signing over computed permission scope data and reguested signer...`)
// the smart account sign over the permittedScope and targetAddress
const permittedScopeSignature: Hex = await signMessage({
privateKey: this.getPrivateKey() as `0x${string}`,
message: { raw: concatHex([keccak256(permittedScopeData), targetAddress]) }
message: { raw: concatHex([keccak256(permittedScopeData), signer]) }
})

const _permissionIndex = BigInt(0)

const encodedData = ethers.utils.defaultAbiCoder.encode(
['uint256', 'tuple(uint48,uint48,address,bytes,address,bytes)', 'bytes', 'bytes'],
[
_permissionIndex,
[
permissions[0].validAfter,
permissions[0].validUntil,
permissions[0].signatureValidationAlgorithm,
permissions[0].signer,
permissions[0].policy,
permissions[0].policyData
],
permittedScopeData,
permittedScopeSignature
]
) as `0x${string}`
console.log(`encoding permissionsContext bytes data...`)
const permissionsContext = concatHex([
PERMISSION_VALIDATOR_ADDRESS,
encodePacked(['uint8', 'bytes'], [1, encodedData])
])
return {
accountType: 'Safe7579',
accountAddress: this.client.account.address,
permissionValidatorAddress: PERMISSION_VALIDATOR_ADDRESS,
permissions: permissions,
permittedScopeData: permittedScopeData,
permittedScopeSignature: permittedScopeSignature
permissionsContext,
permittedScopeSignature,
permittedScopeData,
permissions
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ import { Address } from 'viem'
export const PERMISSION_VALIDATOR_ADDRESS: Address = '0x6671AD9ED29E2d7a894E80bf48b7Bf03Ee64A0f4'
export const SECP256K1_SIGNATURE_VALIDATOR_ADDRESS: Address =
'0x033f60A1035E64c96FD511abA61bA8d9276ADB4f'
export const SAFE7579_USER_OPERATION_BUILDER_ADDRESS: Address =
'0xBe9086ff28A0DEB285fa74fE9f290522AC2693EC'
// export const KERNEL_V3_USER_OPERATION_BUILDER_ADDRESS=""
// export const BICONOMY_USER_OPERATION_BUILDER_ADDRESS=""
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ export type SingleSignerPermission = {
}

export type PermissionContext = {
accountType: 'KernelV3' | 'Safe7579'
accountAddress: Address
initCode?: `0x${string}`
permissionsContext?: `0x${string}`
userOperationBuilder?: Address
// below are rn temporary values needed to cover Kernel SC flow
accountType: 'KernelV3' | 'Safe7579'
permissionValidatorAddress: Address
permissions: SingleSignerPermission[]
permittedScopeData: `0x${string}`
Expand Down
Loading