Skip to content

Commit

Permalink
AA-513 single auth tuple for UserOperation (#232)
Browse files Browse the repository at this point in the history
* AA-453:Support EIP-7702 tuples in ERC-4337 bundlers

* Add 'eip7702Support' config; prototype code for EIP-7702 bundle building

* AA-453: Add proxy for EIP-7702 'sendTransaction' encoding

* Fix sending array object instead of hex string

* Move 7702 list to BaseOperation; fix duplicate tuples in 'createBundle'

* Implement authorization ecrecover and provide state overrides to simulation

* Implement EIP-7702 support

* Remove packed dependencies and use the published betas

* check nonce before auth. fix rlp

* single auth tuple for UserOperation

---------

Co-authored-by: Alex Forshtat <[email protected]>
Co-authored-by: shahafn <[email protected]>
  • Loading branch information
3 people authored Jan 12, 2025
1 parent 2a6a3f4 commit 830ae98
Show file tree
Hide file tree
Showing 14 changed files with 76 additions and 60 deletions.
3 changes: 1 addition & 2 deletions packages/bundler/src/BundlerServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,7 @@ export class BundlerServer {
callGasLimit: 0,
maxFeePerGas: 0,
maxPriorityFeePerGas: 0,
signature: '0x',
authorizationList: []
signature: '0x'
}
// await EntryPoint__factory.connect(this.config.entryPoint,this.provider).callStatic.addStake(0)
try {
Expand Down
11 changes: 6 additions & 5 deletions packages/bundler/src/MethodHandlerERC4337.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
requireCond,
simulationRpcParams,
tostr,
unpackUserOp
unpackUserOp, getAuthorizationList
} from '@account-abstraction/utils'
import { BundlerConfig } from './BundlerConfig'

Expand Down Expand Up @@ -124,7 +124,7 @@ export class MethodHandlerERC4337 {
entryPointInput: string,
stateOverride?: StateOverride
): Promise<EstimateUserOpGasResult> {
if (!this.config.eip7702Support && userOp1.authorizationList != null && userOp1.authorizationList.length !== 0) {
if (!this.config.eip7702Support && userOp1.eip7702auth != null) {
throw new Error('EIP-7702 tuples are not supported')
}
const userOp: UserOperation = {
Expand Down Expand Up @@ -162,6 +162,7 @@ export class MethodHandlerERC4337 {
preOpGas
} = returnInfo

const authorizationList = getAuthorizationList(userOp)
// todo: use simulateHandleOp for this too...
let callGasLimit = await this.provider.send(
'eth_estimateGas', [
Expand All @@ -170,7 +171,7 @@ export class MethodHandlerERC4337 {
to: userOp.sender,
data: userOp.callData,
// @ts-ignore
authorizationList: userOp.authorizationList
authorizationList: authorizationList.length === 0 ? null : authorizationList
}
]
).then(b => toNumber(b)).catch(err => {
Expand All @@ -192,12 +193,12 @@ export class MethodHandlerERC4337 {
}

async sendUserOperation (userOp: UserOperation, entryPointInput: string): Promise<string> {
if (!this.config.eip7702Support && userOp.authorizationList != null && userOp.authorizationList.length !== 0) {
if (!this.config.eip7702Support && userOp.eip7702auth != null) {
throw new Error('EIP-7702 tuples are not supported')
}
await this._validateParameters(userOp, entryPointInput)

debug(`UserOperation: Sender=${userOp.sender} Nonce=${tostr(userOp.nonce)} EntryPoint=${entryPointInput} Paymaster=${userOp.paymaster ?? ''} EIP-7702TuplesSize=${userOp.authorizationList?.length}`)
debug(`UserOperation: Sender=${userOp.sender} Nonce=${tostr(userOp.nonce)} EntryPoint=${entryPointInput} Paymaster=${userOp.paymaster ?? ''} ${userOp.eip7702auth != null ? 'eip-7702 auth' : ''}`)
await this.execManager.sendUserOperation(userOp, entryPointInput, false)
return await this.entryPoint.getUserOpHash(packUserOp(userOp))
}
Expand Down
46 changes: 21 additions & 25 deletions packages/bundler/src/modules/BundleManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
getEip7702AuthorizationSigner,
mergeStorageMap,
packUserOp,
getUserOpHash
getUserOpHash, getAuthorizationList
} from '@account-abstraction/utils'

import { EventsManager } from './EventsManager'
Expand Down Expand Up @@ -228,38 +228,36 @@ export class BundleManager implements IBundleManager {
const common = new Common({ chain, eips: [2718, 2929, 2930, 7702] })

const authorizationList: AuthorizationList = eip7702Tuples.map(it => {
const res = {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion,@typescript-eslint/no-base-to-string
chainId: `0x${parseInt(it.chainId.toString()).toString(16)}` as PrefixedHexString,
address: it.address as PrefixedHexString,
nonce: toRlpHex(it.nonce as PrefixedHexString),
yParity: toRlpHex(it.yParity as PrefixedHexString),
r: it.r as PrefixedHexString,
s: it.s as PrefixedHexString
return {
chainId: toRlpHex(it.chainId),
address: toRlpHex(it.address),
nonce: toRlpHex(it.nonce),
yParity: toRlpHex(it.yParity),
r: toRlpHex(it.r),
s: toRlpHex(it.s)
}
return res
})
const txData: EOACode7702TxData = {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
nonce: `0x${tx.nonce!.toString(16)}`,
nonce: hexlify(tx.nonce!) as PrefixedHexString,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
to: tx.to!.toString() as PrefixedHexString,
to: hexlify(tx.to!) as PrefixedHexString,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
value: '0x0',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
data: tx.data!.toString() as PrefixedHexString,
data: hexlify(tx.data!) as PrefixedHexString,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
chainId: `0x${tx.chainId!.toString(16)}`,
chainId: hexlify(tx.chainId!) as PrefixedHexString,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
maxPriorityFeePerGas: tx.maxPriorityFeePerGas!.toHexString() as PrefixedHexString,
maxPriorityFeePerGas: hexlify(tx.maxPriorityFeePerGas!) as PrefixedHexString,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
maxFeePerGas: tx.maxPriorityFeePerGas!.toHexString() as PrefixedHexString,
maxFeePerGas: hexlify(tx.maxPriorityFeePerGas!) as PrefixedHexString,
accessList: [],
authorizationList
}
// TODO: not clear why but 'eth_estimateGas' gives an 'execution reverted' error
// txData.gasLimit = await this.provider.send('eth_estimateGas', [txData])
txData.gasLimit = `0x${(10000000).toString(16)}`
txData.gasLimit = 10_000_000
const objectTx = new EOACode7702Transaction(txData, { common })
const privateKey = Buffer.from(
// @ts-ignore
Expand Down Expand Up @@ -463,17 +461,15 @@ export class BundleManager implements IBundleManager {
* @return {boolean} - Returns `true` if the authorizations were successfully merged, otherwise `false`.
*/
mergeEip7702Authorizations (entry: MempoolEntry, authList: EIP7702Authorization[]): boolean {
for (const eip7702Authorization of entry.userOp.authorizationList ?? []) {
const authorizationList = getAuthorizationList(entry.userOp)
for (const eip7702Authorization of authorizationList) {
const existingAuthorization = authList
.find(it => {
return getEip7702AuthorizationSigner(it) === getEip7702AuthorizationSigner(eip7702Authorization)
})
if (existingAuthorization != null && existingAuthorization.address.toLowerCase() !== eip7702Authorization.address.toLowerCase()) {
.find(it => getEip7702AuthorizationSigner(it) === getEip7702AuthorizationSigner(eip7702Authorization))
if (existingAuthorization == null) {
authList.push(eip7702Authorization)
} else if (existingAuthorization.address.toLowerCase() !== eip7702Authorization.address.toLowerCase()) {
return false
}
if (existingAuthorization == null && entry.userOp.authorizationList != null) {
authList.push(...entry.userOp.authorizationList)
}
}
return true
}
Expand Down
6 changes: 2 additions & 4 deletions packages/bundler/test/BundlerManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ describe('#BundlerManager', () => {
verificationGasLimit: 7,
maxFeePerGas: 8,
maxPriorityFeePerGas: 9,
preVerificationGas: 10,
authorizationList: []
preVerificationGas: 10
}

const hash = await entryPoint.getUserOpHash(packUserOp(userOp))
Expand Down Expand Up @@ -161,8 +160,7 @@ describe('#BundlerManager', () => {
verificationGasLimit: '0x50000',
maxFeePerGas: '0x0',
maxPriorityFeePerGas: '0x0',
preVerificationGas: '0x50000',
authorizationList: []
preVerificationGas: '0x50000'
}
const userOp1: UserOperation = {
...cEmptyUserOp,
Expand Down
3 changes: 1 addition & 2 deletions packages/bundler/test/UserOpMethodHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,7 @@ describe('UserOpMethodHandler', function () {
preVerificationGas: 50000,
maxFeePerGas: 1e6,
maxPriorityFeePerGas: 1e6,
signature: Buffer.from('emit-msg'),
authorizationList: []
signature: Buffer.from('emit-msg')
}
await entryPoint.depositTo(acc.address, { value: parseEther('1') })
// await signer.sendTransaction({to:acc.address, value: parseEther('1')})
Expand Down
3 changes: 1 addition & 2 deletions packages/bundler/test/ValidateManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ const cEmptyUserOp: UserOperation = {
verificationGasLimit: 50000,
maxFeePerGas: 0,
maxPriorityFeePerGas: 0,
preVerificationGas: 0,
authorizationList: []
preVerificationGas: 0
}

describe('#ValidationManager', () => {
Expand Down
6 changes: 2 additions & 4 deletions packages/sdk/test/0-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ describe('utils', () => {
callData: '333',
maxFeePerGas: 5,
maxPriorityFeePerGas: 6,
signature: '777',
authorizationList: []
signature: '777'
})).to.eql({
sender: 'a',
nonce: '0x01',
Expand Down Expand Up @@ -95,8 +94,7 @@ describe('utils', () => {
paymaster,
paymasterVerificationGasLimit: 8,
paymasterPostOpGasLimit: 9,
paymasterData: '0xcafebabe',
authorizationList: []
paymasterData: '0xcafebabe'
})).to.eql({
sender: 'a',
nonce: '0x01',
Expand Down
3 changes: 1 addition & 2 deletions packages/sdk/test/1-SimpleAccountAPI.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ describe('SimpleAccountAPI', () => {
preVerificationGas: 7,
maxFeePerGas: 8,
maxPriorityFeePerGas: 9,
signature: '0xbbbb',
authorizationList: []
signature: '0xbbbb'
}
const hash = await api.getUserOpHash(userOp)
const epHash = await entryPoint.getUserOpHash(packUserOp(userOp))
Expand Down
5 changes: 2 additions & 3 deletions packages/utils/src/ERC4337Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,7 @@ export function unpackUserOp (packed: PackedUserOperation): UserOperation {
callGasLimit,
maxFeePerGas,
maxPriorityFeePerGas,
signature: packed.signature,
authorizationList: []
signature: packed.signature
}
if (packed.initCode != null && packed.initCode.length > 2) {
const factory = hexDataSlice(packed.initCode, 0, 20)
Expand All @@ -201,7 +200,7 @@ export function unpackUserOp (packed: PackedUserOperation): UserOperation {

/**
* abi-encode the userOperation
* @param op a PackedUserOp
* @param op1 a PackedUserOp
* @param forSignature "true" if the hash is needed to calculate the getUserOpHash()
* "false" to pack entire UserOp, for calculating the calldata cost of putting it on-chain.
*/
Expand Down
10 changes: 10 additions & 0 deletions packages/utils/src/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { PackedUserOperationStruct } from './soltypes'
import { UserOperation } from './interfaces/UserOperation'
import { OperationBase } from './interfaces/OperationBase'
import { OperationRIP7560 } from './interfaces/OperationRIP7560'
import { EIP7702Authorization } from './interfaces/EIP7702Authorization'

export interface SlotMap {
[slot: string]: string
Expand Down Expand Up @@ -236,3 +237,12 @@ export function getPackedNonce (userOp: OperationBase): BigNumber {
const bigNumberNonce = BigNumber.from(packed)
return bigNumberNonce
}

export function getAuthorizationList (op: OperationBase): EIP7702Authorization[] {
const userOp = op as UserOperation
if (userOp.eip7702auth != null) {
return [userOp.eip7702auth]
} else {
return (op as OperationRIP7560).authorizationList ?? []
}
}
1 change: 0 additions & 1 deletion packages/utils/src/interfaces/OperationBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,4 @@ export interface OperationBase {
verificationGasLimit: BigNumberish
paymasterVerificationGasLimit?: BigNumberish
paymasterPostOpGasLimit?: BigNumberish
authorizationList?: EIP7702Authorization[]
}
3 changes: 3 additions & 0 deletions packages/utils/src/interfaces/OperationRIP7560.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OperationBase } from './OperationBase'
import { BigNumberish, BytesLike } from 'ethers'
import { EIP7702Authorization } from './EIP7702Authorization'

export interface OperationRIP7560 extends OperationBase {
chainId: BigNumberish
Expand All @@ -12,4 +13,6 @@ export interface OperationRIP7560 extends OperationBase {

// todo: we discussed using 'nonceKey' in the JSON schema for ERC-4337 as well but we did not finalize this decision
nonceKey: BigNumberish

authorizationList?: EIP7702Authorization[]
}
2 changes: 2 additions & 0 deletions packages/utils/src/interfaces/UserOperation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BigNumberish, BytesLike } from 'ethers'
import { OperationBase } from './OperationBase'
import { EIP7702Authorization } from './EIP7702Authorization'

export interface UserOperation extends OperationBase {
// these fields have same meaning but different names between ERC-4337 and RIP-7560/RIP-7712
Expand All @@ -8,4 +9,5 @@ export interface UserOperation extends OperationBase {
nonce: BigNumberish

preVerificationGas: BigNumberish
eip7702auth?: EIP7702Authorization
}
34 changes: 24 additions & 10 deletions packages/validation-manager/src/ValidationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
EIP7702Authorization,
IEntryPoint,
IEntryPointSimulations__factory,
OperationBase,
ReferencedCodeHashes,
RpcError,
StakeInfo,
Expand All @@ -27,8 +28,7 @@ import {
packUserOp,
requireAddressAndFields,
requireCond,
runContractScript,
OperationBase, SenderCreator__factory, IEntryPoint__factory, IPaymaster__factory
runContractScript, getAuthorizationList, SenderCreator__factory, IEntryPoint__factory, IPaymaster__factory
} from '@account-abstraction/utils'

import { tracerResultParser } from './TracerResultParser'
Expand Down Expand Up @@ -56,12 +56,14 @@ const entryPointSimulations = IEntryPointSimulations__factory.createInterface()
* (relevant only if unsafe=false)
*/
export class ValidationManager implements IValidationManager {
private readonly provider: JsonRpcProvider
constructor (
readonly entryPoint: IEntryPoint,
readonly unsafe: boolean,
readonly preVerificationGasCalculator: PreVerificationGasCalculator,
readonly providerForTracer?: JsonRpcProvider
) {
this.provider = this.entryPoint.provider as JsonRpcProvider
}

_getDebugConfiguration (): {
Expand Down Expand Up @@ -118,8 +120,7 @@ export class ValidationManager implements IValidationManager {
}
}
try {
const provider = this.entryPoint.provider as JsonRpcProvider
const simulationResult = await provider.send('eth_call', [tx, 'latest', stateOverride])
const simulationResult = await this.provider.send('eth_call', [tx, 'latest', stateOverride])
const [res] = entryPointSimulations.decodeFunctionResult('simulateValidation', simulationResult) as ValidationResultStructOutput[]

return this.parseValidationResult(userOp, res)
Expand Down Expand Up @@ -237,7 +238,22 @@ export class ValidationManager implements IValidationManager {
addresses: [],
hash: ''
}
const stateOverrideForEip7702 = await this.getAuthorizationsStateOverride(userOp.authorizationList ?? [])
const authorizationList = getAuthorizationList(userOp)
if (authorizationList.length > 0) {
// relevant only for RIP-7560...
requireCond(authorizationList.length === 1, 'Only one authorization is supported', ValidationErrors.InvalidFields)

const chainId = await this.provider.getNetwork().then(n => n.chainId)

// list is required to be of size=1. for completeness, we still scan it as a list.
for (const authorization of authorizationList) {
const authChainId = BigNumber.from(authorization.chainId)
requireCond(authChainId.eq(BigNumber.from(0)) ||
authChainId.eq(chainId), 'Invalid chainId in authorization', ValidationErrors.InvalidFields)
requireCond(getEip7702AuthorizationSigner(authorizationList[0]).toLowerCase() === userOp.sender.toLowerCase(), 'Authorization signer is not sender', ValidationErrors.InvalidFields)
}
}
const stateOverrideForEip7702 = await this.getAuthorizationsStateOverride(authorizationList)
let storageMap: StorageMap = {}
if (!this.unsafe) {
let tracerResult: BundlerTracerResult
Expand Down Expand Up @@ -301,17 +317,15 @@ export class ValidationManager implements IValidationManager {
authorizations: EIP7702Authorization[] = []
): Promise<{ [address: string]: { code: string } }> {
const stateOverride: { [address: string]: { code: string } } = {}
// TODO: why don't we have 'provider' as a member in here?
const provider = this.entryPoint.provider as JsonRpcProvider
for (const authorization of authorizations) {
const authSigner = getEip7702AuthorizationSigner(authorization)
const nonce = await provider.getTransactionCount(authSigner)
const nonce = await this.provider.getTransactionCount(authSigner)
const authNonce: any = authorization.nonce
if (nonce !== BigNumber.from(authNonce.replace(/0x$/, '0x0')).toNumber()) {
continue
}
const currentDelegateeCode = await provider.getCode(authSigner)
const newDelegateeCode = await provider.getCode(authorization.address)
const currentDelegateeCode = await this.provider.getCode(authSigner)
const newDelegateeCode = await this.provider.getCode(authorization.address)
// TODO should be: hexConcat(['0xef0100', authorization.address])
const noCurrentDelegation = currentDelegateeCode.length <= 2
// TODO: do not send such authorizations to 'handleOps' as it is a waste of gas
Expand Down

0 comments on commit 830ae98

Please sign in to comment.