Skip to content

Commit

Permalink
Implement EIP-7702 support
Browse files Browse the repository at this point in the history
  • Loading branch information
forshtat committed Sep 24, 2024
1 parent 1db360c commit 298d55a
Show file tree
Hide file tree
Showing 17 changed files with 123 additions and 63 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"eslint-plugin-n": "^15.2.4",
"eslint-plugin-promise": "^6.0.0",
"lerna": "^5.4.0",
"typescript": "^4.7.4",
"typescript": "^5.6.2",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
},
Expand Down
34 changes: 2 additions & 32 deletions packages/bundler/src/BundlerServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import cors from 'cors'
import express, { Express, Response, Request, RequestHandler } from 'express'
import { JsonRpcProvider } from '@ethersproject/providers'
import { Signer, utils } from 'ethers'
import { hexlify, parseEther } from 'ethers/lib/utils'
import { ChainConfig, Common, Hardfork, Mainnet } from '@ethereumjs/common'
import { EOACode7702Transaction } from '@ethereumjs/tx'
import { parseEther } from 'ethers/lib/utils'
import { Server } from 'http'

import {
Expand All @@ -17,7 +15,7 @@ import {
decodeRevertReason,
deepHexlify,
erc4337RuntimeVersion,
packUserOp, sleep
packUserOp
} from '@account-abstraction/utils'

import { BundlerConfig } from './BundlerConfig'
Expand Down Expand Up @@ -246,34 +244,6 @@ export class BundlerServer {
break
}
case 'eth_sendTransaction':
if (params[0].authorizationList != null) {
console.log('eth_sendTransaction received EIP-7702 transaction', JSON.stringify(params[0]))
// NOTE: @ethereumjs/tx v5.4.0 has a 'tuple nonce' as an array - patch or wait for fix
// @ts-ignore
const chain: ChainConfig = {
bootstrapNodes: [],
defaultHardfork: Hardfork.Prague,
// consensus: undefined,
// genesis: undefined,
hardforks: Mainnet.hardforks,
name: '',
chainId: 1337
}
const common = new Common({ chain, eips: [2718, 2929, 2930, 7702] })
const objectTx = new EOACode7702Transaction(params[0], { common })
const privateKey = Buffer.from(
'e331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109',
'hex'
)
const signedTx = objectTx.sign(privateKey)
const encodedTx = signedTx.serialize()
const senderAddress = signedTx.getSenderAddress().toString()
await this.wallet.sendTransaction({ to: senderAddress, value: parseEther('0.1') })
await sleep(3000)
const rawTransaction = hexlify(encodedTx)
result = await this.provider.send('eth_sendRawTransaction', [rawTransaction])
break
}
if (!this.config.rip7560) {
throw new RpcError(`Method ${method} is not supported`, -32601)
}
Expand Down
21 changes: 15 additions & 6 deletions packages/bundler/src/MethodHandlerERC4337.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,17 +146,26 @@ export class MethodHandlerERC4337 {

const returnInfo = decodeSimulateHandleOpResult(ret)

const { validAfter, validUntil } = mergeValidationDataValues(returnInfo.accountValidationData, returnInfo.paymasterValidationData)
const {
validAfter,
validUntil
} = mergeValidationDataValues(returnInfo.accountValidationData, returnInfo.paymasterValidationData)
const {
preOpGas
} = returnInfo

// todo: use simulateHandleOp for this too...
const callGasLimit = await this.provider.estimateGas({
from: this.entryPoint.address,
to: userOp.sender,
data: userOp.callData
}).then(b => b.toNumber()).catch(err => {
const callGasLimit = await this.provider.send(
'eth_estimateGas', [
{
from: this.entryPoint.address,
to: userOp.sender,
data: userOp.callData,
// @ts-ignore
authorizationList: userOp.authorizationList
}
]
).then(b => b.toNumber()).catch(err => {
const message = err.message.match(/reason="(.*?)"/)?.at(1) ?? 'execution reverted'
throw new RpcError(message, ValidationErrors.UserOperationReverted)
})
Expand Down
84 changes: 82 additions & 2 deletions packages/bundler/src/modules/BundleManager.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Debug from 'debug'
import { BigNumber, BigNumberish, Signer } from 'ethers'
import { BigNumber, BigNumberish, PopulatedTransaction, Signer } from 'ethers'
import { ErrorDescription } from '@ethersproject/abi/lib/interface'
import { JsonRpcProvider } from '@ethersproject/providers'
import { Mutex } from 'async-mutex'
import { isAddress } from 'ethers/lib/utils'
import { hexlify, isAddress } from 'ethers/lib/utils'

import {
EmptyValidateUserOpResult,
Expand Down Expand Up @@ -32,6 +32,10 @@ import { IBundleManager } from './IBundleManager'
import { MempoolEntry } from './MempoolEntry'
import { MempoolManager } from './MempoolManager'
import { ReputationManager, ReputationStatus } from './ReputationManager'
import { ChainConfig, Common, Hardfork, Mainnet } from '@ethereumjs/common'
import { EOACode7702Transaction } from '@ethereumjs/tx'
import { AuthorizationList, EOACode7702TxData } from '@ethereumjs/tx/src/types'
import { PrefixedHexString } from '@ethereumjs/util'

const debug = Debug('aa.exec.cron')

Expand Down Expand Up @@ -123,6 +127,11 @@ export class BundleManager implements IBundleManager {
signedTx, { knownAccounts: storageMap }
])
debug('eth_sendRawTransactionConditional ret=', ret)
} else if (tx.type === TX_TYPE_EIP_7702) {
const ethereumJsTx = await this._prepareEip7702Transaction(tx, eip7702Tuples)
const res = await this.provider.send('eth_sendRawTransaction', [ethereumJsTx])
const rcpt = await this.provider.getTransactionReceipt(res)
ret = rcpt.transactionHash
} else {
const resp = await this.signer.sendTransaction(tx)
const rcpt = await resp.wait()
Expand Down Expand Up @@ -175,6 +184,75 @@ export class BundleManager implements IBundleManager {
}
}

// TODO: this is a temporary patch until ethers.js adds EIP-7702 support
async _prepareEip7702Transaction (tx: PopulatedTransaction, eip7702Tuples: EIP7702Authorization[]): Promise<string> {
console.log('creating EIP-7702 transaction')
// TODO: read fields from the configuration
// @ts-ignore
const chain: ChainConfig = {
bootstrapNodes: [],
defaultHardfork: Hardfork.Prague,
// consensus: undefined,
// genesis: undefined,
hardforks: Mainnet.hardforks,
name: '',
chainId: 1337
}
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: it.nonce as PrefixedHexString,
yParity: it.yParity as PrefixedHexString,
r: it.r as PrefixedHexString,
s: it.s as PrefixedHexString
}
if (res.yParity === '0x0') {
// o, for fuck's sake!
res.yParity = '0x'
}
if (res.nonce === '0x0') {
throw new Error('ethereumjs/tx does not handle zero nonce!')
}
return res
})
const txData: EOACode7702TxData = {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
nonce: `0x${tx.nonce!.toString(16)}`,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
to: tx.to!.toString() 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,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
chainId: `0x${tx.chainId!.toString(16)}`,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
maxPriorityFeePerGas: tx.maxPriorityFeePerGas!.toHexString() as PrefixedHexString,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
maxFeePerGas: tx.maxPriorityFeePerGas!.toHexString() 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)}`
const objectTx = new EOACode7702Transaction(txData, { common })
const privateKey = Buffer.from(
// @ts-ignore
this.signer.privateKey.slice(2),
// 'e331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109',
'hex'
)

const signedTx = objectTx.sign(privateKey)
const encodedTx = signedTx.serialize()
return hexlify(encodedTx)
// const senderAddress = signedTx.getSenderAddress().toString()
}

async _findEntityToBlame (reasonStr: string, userOp: UserOperation): Promise<string | undefined> {
if (reasonStr.startsWith('AA3')) {
// [EREP-030] A staked account is accountable for failure in any entity
Expand Down Expand Up @@ -257,11 +335,13 @@ export class BundleManager implements IBundleManager {
continue
}
// [GREP-020] - renamed from [SREP-030]
// @ts-ignore
if (paymaster != null && (paymasterStatus === ReputationStatus.THROTTLED ?? (stakedEntityCount[paymaster] ?? 0) > THROTTLED_ENTITY_BUNDLE_COUNT)) {
debug('skipping throttled paymaster', entry.userOp.sender, (entry.userOp as any).nonce)
continue
}
// [GREP-020] - renamed from [SREP-030]
// @ts-ignore
if (factory != null && (deployerStatus === ReputationStatus.THROTTLED ?? (stakedEntityCount[factory] ?? 0) > THROTTLED_ENTITY_BUNDLE_COUNT)) {
debug('skipping throttled factory', entry.userOp.sender, (entry.userOp as any).nonce)
continue
Expand Down
2 changes: 1 addition & 1 deletion packages/bundler/src/modules/MempoolManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ export class MempoolManager {
res.push(
...userOps.map(it => it.userOp.factory)
)

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
return res.filter(it => it != null).map(it => (it as string).toLowerCase())
}

Expand Down
1 change: 1 addition & 0 deletions packages/bundler/test/UserOpMethodHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ describe('UserOpMethodHandler', function () {
// sendUserOperation is async, even in auto-mining. need to wait for it.
const event = await waitFor(async () => await entryPoint.queryFilter(entryPoint.filters.UserOperationEvent(userOpHash)).then(ret => ret?.[0]))

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const transactionReceipt = await event!.getTransactionReceipt()
assert.isNotNull(transactionReceipt)
const logs = transactionReceipt.logs.filter(log => log.address === entryPoint.address)
Expand Down
2 changes: 1 addition & 1 deletion packages/bundler/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2017",
"target": "es2020",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
Expand Down
2 changes: 1 addition & 1 deletion packages/bundler/tsconfig.packages.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2017",
"target": "es2020",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2017",
"target": "es2020",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/tsconfig.packages.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2017",
"target": "es2020",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
Expand Down
10 changes: 4 additions & 6 deletions packages/utils/src/interfaces/EIP7702Authorization.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { BigNumberish } from 'ethers'
import RLP from 'rlp'
import { toHex } from 'hardhat/internal/util/bigint'
import { hashMessage, hexlify } from 'ethers/lib/utils'
import { bytesToHex, ecrecover, hexToBytes, pubToAddress } from '@ethereumjs/util'
import { bytesToHex, ecrecover, hexToBigInt, hexToBytes, pubToAddress } from '@ethereumjs/util'
import { AddressZero } from '../ERC4337Utils'
import { keccak256 } from '@ethersproject/keccak256'

Expand All @@ -27,11 +25,11 @@ export function getEip7702AuthorizationSigner (authorization: EIP7702Authorizati
)
]
const messageHash = keccak256(rlpEncode) as `0x${string}`
console.log(hexlify(rlpEncode))
console.log(messageHash)
// console.log('getEip7702AuthorizationSigner RLP:\n', hexlify(rlpEncode), rlpEncode.length)
// console.log('getEip7702AuthorizationSigner hash:\n', messageHash)
const senderPubKey = ecrecover(
hexToBytes(messageHash),
BigInt(authorization.yParity.toString()),
hexToBigInt(authorization.yParity.toString() as `0x${string}`),
hexToBytes(authorization.r.toString() as `0x${string}`),
hexToBytes(authorization.s.toString() as `0x${string}`)
)
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
],
"compilerOptions": {
"outDir": "dist",
"target": "es2017",
"target": "es2020",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
Expand Down
12 changes: 7 additions & 5 deletions packages/validation-manager/src/ValidationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export class ValidationManager implements IValidationManager {

async _geth_traceCall_SimulateValidation (
operation: OperationBase,
stateOverride: {[address: string]: {code: string}}
stateOverride: { [address: string]: { code: string } }
): Promise<[ValidationResult, BundlerTracerResult]> {
const userOp = operation as UserOperation
const provider = this.entryPoint.provider as JsonRpcProvider
Expand Down Expand Up @@ -251,8 +251,10 @@ export class ValidationManager implements IValidationManager {
}
}

async getAuthorizationsStateOverride(authorizations: EIP7702Authorization[]): Promise<{[address: string]: {code: string}}> {
const stateOverride: {[address: string]: {code: string}} = {}
async getAuthorizationsStateOverride (
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) {
Expand All @@ -262,8 +264,8 @@ export class ValidationManager implements IValidationManager {
const noCurrentDelegation = currentDelegateeCode.length <= 2
// TODO: do not send such authorizations to 'handleOps' as it is a waste of gas
const changeDelegation = newDelegateeCode !== currentDelegateeCode
if (noCurrentDelegation || changeDelegation){
console.log('Adding state override:', {address: sender, code: newDelegateeCode.slice(0, 20)})
if (noCurrentDelegation || changeDelegation) {
console.log('Adding state override:', { address: sender, code: newDelegateeCode.slice(0, 20) })
stateOverride[sender] = {
code: newDelegateeCode
}
Expand Down
2 changes: 1 addition & 1 deletion packages/validation-manager/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2017",
"target": "es2020",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
Expand Down
2 changes: 1 addition & 1 deletion packages/validation-manager/tsconfig.packages.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2017",
"target": "es2020",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"jsx": "react",
"target": "es2017",
"target": "es2020",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
Expand Down
4 changes: 2 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10597,12 +10597,12 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==

typescript@>=4.5.0:
typescript@>=4.5.0, typescript@^5.6.2:
version "5.6.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0"
integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==

"typescript@^3 || ^4", typescript@^4.7.4:
"typescript@^3 || ^4":
version "4.9.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
Expand Down

0 comments on commit 298d55a

Please sign in to comment.