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

AA-513 single auth tuple for UserOperation #232

Merged
merged 28 commits into from
Jan 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1e088a5
AA-453: (WIP) Support EIP-7702 tuples in ERC-4337 bundlers
forshtat Sep 20, 2024
af61ec4
Add 'eip7702Support' config; prototype code for EIP-7702 bundle building
forshtat Sep 20, 2024
7db8ae9
AA-453: (WIP) (TMP) Add proxy for EIP-7702 'sendTransaction' encoding
forshtat Sep 21, 2024
97b472f
Fix sending array object instead of hex string
forshtat Sep 21, 2024
51a73ea
Move 7702 list to BaseOperation; fix duplicate tuples in 'createBundle'
forshtat Sep 22, 2024
18266c4
Lint
forshtat Sep 22, 2024
fe0402a
WIP: Actually sending a valid EIP-7702 transaction (tgz dependencies …
forshtat Sep 22, 2024
1849384
TMP: Send funds to temp sender
forshtat Sep 23, 2024
1db360c
Implement authorization ecrecover and provide state overrides to simu…
forshtat Sep 24, 2024
298d55a
Implement EIP-7702 support
forshtat Sep 24, 2024
9a12e26
Remove packed dependencies and use the published betas
forshtat Dec 8, 2024
f46bd6e
check nonce before auth. fix rlp
drortirosh Dec 10, 2024
24a80f8
Fix wrong 'eip7702Support' checks, clean up comments
forshtat Dec 11, 2024
915df43
Merge branch 'master' of github.com:eth-infinitism/bundler into AA-45…
forshtat Dec 11, 2024
2cc729c
Fix depcheck
forshtat Dec 11, 2024
af7d3d3
Fix tests
forshtat Dec 11, 2024
9a5bc12
nullable authorizationList
drortirosh Dec 12, 2024
d2e5987
single auth tuple for UserOperation
drortirosh Dec 16, 2024
0ec75d0
typo
drortirosh Dec 18, 2024
426ed01
Merge branch 'master' into single-auth-tuple
drortirosh Jan 2, 2025
bb91971
auth list validation rules
drortirosh Jan 2, 2025
dcfe286
lint
drortirosh Jan 5, 2025
c8d37f6
PR issues
drortirosh Jan 6, 2025
5086a49
restore mergeEip7702Authorizations
drortirosh Jan 6, 2025
d4be94d
pr review
drortirosh Jan 8, 2025
6541250
fix authlist
drortirosh Jan 9, 2025
8e2d3c7
Merge branch 'master' into single-auth-tuple
shahafn Jan 12, 2025
ef4dbb6
Fix comment
shahafn Jan 12, 2025
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
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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Why? we have ValidationManager7560, that will handle it as a list, why do we need it here?

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
Loading