Skip to content

Commit 828190f

Browse files
authored
AA-110-check-codehash (eth-infinitism#42)
* AA-110-check-codehash check code of all accessed code: - must not be zero-sized (unless it is a precompile) - are not modified before the 2nd validation (that is, fail the userOp before 2nd validation if a contract code was modified) sendBundleNow returns { transactionHash, userOpHashes }
1 parent 1a9d87c commit 828190f

20 files changed

+246
-75
lines changed

.idea/aa-bundler.iml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/bundler/.depcheckrc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
ignores: ["solidity-string-utils"]
Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,22 @@
11
// SPDX-License-Identifier: GPL-3.0
22
pragma solidity ^0.8.15;
33

4-
import "@account-abstraction/contracts/core/EntryPoint.sol";
5-
import "solidity-string-utils/StringUtils.sol";
4+
import "@account-abstraction/contracts/interfaces/IEntryPoint.sol";
65

76
contract BundlerHelper {
8-
using StringUtils for *;
9-
10-
/**
11-
* run handleop. require to get refund for the used gas.
12-
*/
13-
function handleOps(uint expectedPaymentGas, EntryPoint ep, UserOperation[] calldata ops, address payable beneficiary)
14-
public returns (uint paid, uint gasPrice, bytes memory errorReason){
15-
gasPrice = tx.gasprice;
16-
uint expectedPayment = expectedPaymentGas * gasPrice;
17-
uint preBalance = beneficiary.balance;
18-
try ep.handleOps(ops, beneficiary) {
19-
} catch (bytes memory err) {
20-
errorReason = err;
7+
function getUserOpHashes(IEntryPoint entryPoint, UserOperation[] memory userOps) external view returns (bytes32[] memory ret) {
8+
ret = new bytes32[](userOps.length);
9+
for (uint i = 0; i < userOps.length; i++) {
10+
ret[i] = entryPoint.getUserOpHash(userOps[i]);
2111
}
22-
paid = beneficiary.balance - preBalance;
23-
if (paid < expectedPayment) {
24-
revert(string.concat(
25-
"didn't pay enough: paid ", paid.toString(),
26-
" expected ", expectedPayment.toString(),
27-
" gasPrice ", gasPrice.toString()
28-
));
12+
}
13+
14+
function getCodeHashes(address[] memory addresses) public view returns (bytes32) {
15+
bytes32[] memory hashes = new bytes32[](addresses.length);
16+
for (uint i = 0; i < addresses.length; i++) {
17+
hashes[i] = addresses[i].codehash;
2918
}
19+
bytes memory data = abi.encode(hashes);
20+
return keccak256(data);
3021
}
3122
}

packages/bundler/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@
5656
"hardhat": "^2.11.0",
5757
"hardhat-deploy": "^0.11.11",
5858
"solidity-coverage": "^0.7.21",
59-
"solidity-string-utils": "^0.0.8-0",
6059
"ts-node": ">=8.0.0",
6160
"typechain": "^8.1.0"
6261
}

packages/bundler/src/BundlerCollectorTracer.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { LogCallFrame, LogContext, LogDb, LogFrameResult, LogStep, LogTracer } f
99
declare function toHex (a: any): string
1010

1111
declare function toAddress (a: any): string
12+
13+
declare function isPrecompiled (addr: any): boolean
14+
1215
/**
1316
* return type of our BundlerCollectorTracer.
1417
* collect access and opcodes, split into "levels" based on NUMBER opcode
@@ -42,6 +45,7 @@ export interface ExitInfo {
4245
gasUsed: number
4346
data: string
4447
}
48+
4549
export interface NumberLevelInfo {
4650
opcodes: { [opcode: string]: number }
4751
access: { [address: string]: AccessInfo }
@@ -151,6 +155,16 @@ export function bundlerCollectorTracer (): BundlerCollectorTracer {
151155
}
152156
}
153157

158+
if (opcode.match(/^(EXT.*|CALL|CALLCODE|DELEGATECALL|STATICCALL)$/) != null) {
159+
// this.debug.push('op=' + opcode + ' last=' + this.lastOp + ' stacksize=' + log.stack.length())
160+
const idx = opcode.startsWith('EXT') ? 0 : 1
161+
const addr = toAddress(log.stack.peek(idx).toString(16))
162+
const addrHex = toHex(addr)
163+
if ((this.currentLevel.contractSize[addrHex] ?? 0) === 0 && !isPrecompiled(addr)) {
164+
this.currentLevel.contractSize[addrHex] = db.getCode(addr).length
165+
}
166+
}
167+
154168
if (log.getDepth() === 1) {
155169
// NUMBER opcode at top level split levels
156170
if (opcode === 'NUMBER') this.numberCounter++
@@ -175,15 +189,6 @@ export function bundlerCollectorTracer (): BundlerCollectorTracer {
175189
this.countSlot(this.currentLevel.opcodes, opcode)
176190
}
177191
}
178-
if (opcode.match(/^(EXT.*|CALL|CALLCODE|DELEGATECALL|STATICCALL|CREATE2)$/) != null) {
179-
// this.debug.push('op=' + opcode + ' last=' + this.lastOp + ' stacksize=' + log.stack.length())
180-
const idx = opcode.startsWith('EXT') ? 0 : 1
181-
const addr = toAddress(log.stack.peek(idx).toString(16))
182-
const addrHex = toHex(addr)
183-
if (this.currentLevel.contractSize[addrHex] == null) {
184-
this.currentLevel.contractSize[addrHex] = db.getCode(addr).length
185-
}
186-
}
187192
this.lastOp = opcode
188193

189194
if (opcode === 'SLOAD' || opcode === 'SSTORE') {

packages/bundler/src/BundlerConfig.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import ow from 'ow'
44
export interface BundlerConfig {
55
beneficiary: string
66
entryPoint: string
7+
bundlerHelper: string
78
gasFactor: string
89
minBalance: string
910
mnemonic: string
@@ -24,6 +25,7 @@ export interface BundlerConfig {
2425
export const BundlerConfigShape = {
2526
beneficiary: ow.string,
2627
entryPoint: ow.string,
28+
bundlerHelper: ow.string,
2729
gasFactor: ow.string,
2830
minBalance: ow.string,
2931
mnemonic: ow.string,
@@ -45,5 +47,6 @@ export const BundlerConfigShape = {
4547
export const bundlerConfigDefault: Partial<BundlerConfig> = {
4648
port: '3000',
4749
entryPoint: '0x1306b01bC3e4AD202612D3843387e94737673F53',
50+
bundlerHelper: '0x3ac2913fd3ed9a2c6eb7757bcfc6f9cd49cbfea4',
4851
unsafe: false
4952
}

packages/bundler/src/BundlerServer.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,7 @@ export class BundlerServer {
171171
result = 'ok'
172172
break
173173
case 'debug_bundler_sendBundleNow':
174-
await this.debugHandler.sendBundleNow()
175-
result = 'ok'
174+
result = await this.debugHandler.sendBundleNow()
176175
break
177176
default:
178177
throw new RpcError(`Method ${method} is not supported`, -32601)

packages/bundler/src/DebugMethodHandler.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ExecutionManager } from './modules/ExecutionManager'
22
import { ReputationDump, ReputationManager } from './modules/ReputationManager'
33
import { MempoolManager } from './modules/MempoolManager'
4+
import { SendBundleReturn } from './modules/BundleManager'
45

56
export class DebugMethodHandler {
67
constructor (
@@ -29,8 +30,8 @@ export class DebugMethodHandler {
2930
}
3031
}
3132

32-
async sendBundleNow (): Promise<void> {
33-
await this.execManager.attemptBundle(true)
33+
async sendBundleNow (): Promise<SendBundleReturn | undefined> {
34+
return await this.execManager.attemptBundle(true)
3435
}
3536

3637
clearState (): void {

packages/bundler/src/modules/BundleManager.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,23 @@ import Debug from 'debug'
88
import { ReputationManager, ReputationStatus } from './ReputationManager'
99
import { AddressZero } from '@account-abstraction/utils'
1010
import { Mutex } from 'async-mutex'
11+
import { BundlerHelper } from '../types'
1112

1213
const debug = Debug('aa.cron')
1314

15+
export interface SendBundleReturn {
16+
transactionHash: string
17+
userOpHashes: string[]
18+
}
19+
1420
export class BundleManager {
1521
provider: JsonRpcProvider
1622
signer: JsonRpcSigner
1723
mutex = new Mutex()
1824

1925
constructor (
2026
readonly entryPoint: EntryPoint,
27+
readonly bundlerHelper: BundlerHelper,
2128
readonly mempoolManager: MempoolManager,
2229
readonly validationManager: ValidationManager,
2330
readonly reputationManager: ReputationManager,
@@ -34,30 +41,38 @@ export class BundleManager {
3441
* collect UserOps from mempool into a bundle
3542
* send this bundle.
3643
*/
37-
async sendNextBundle (): Promise<void> {
38-
await this.mutex.runExclusive(async () => {
44+
async sendNextBundle (): Promise<SendBundleReturn | undefined> {
45+
return await this.mutex.runExclusive(async () => {
3946
debug('sendNextBundle')
4047

4148
const bundle = await this.createBundle()
4249
if (bundle.length === 0) {
4350
debug('sendNextBundle - no bundle to send')
4451
} else {
4552
const beneficiary = await this._selectBeneficiary()
46-
await this.sendBundle(bundle, beneficiary)
53+
const ret = await this.sendBundle(bundle, beneficiary)
4754
debug(`sendNextBundle exit - after sent a bundle of ${bundle.length} `)
55+
return ret
4856
}
4957
})
5058
}
5159

5260
/**
5361
* submit a bundle.
5462
* after submitting the bundle, remove all UserOps from the mempool
63+
* @return SendBundleReturn the transaction and UserOp hashes on successful transaction, or null on failed transaction
5564
*/
56-
async sendBundle (userOps: UserOperation[], beneficiary: string): Promise<void> {
65+
async sendBundle (userOps: UserOperation[], beneficiary: string): Promise<SendBundleReturn | undefined> {
5766
try {
58-
await this.entryPoint.handleOps(userOps, beneficiary)
67+
const ret = await this.entryPoint.handleOps(userOps, beneficiary)
5968
debug('sent handleOps with', userOps.length, 'ops. removing from mempool')
6069
this.mempoolManager.removeAllUserOps(userOps)
70+
// hashes are needed for debug rpc only.
71+
const hashes = await this.bundlerHelper.getUserOpHashes(this.entryPoint.address, userOps)
72+
return {
73+
transactionHash: ret.hash,
74+
userOpHashes: hashes
75+
}
6176
} catch (e: any) {
6277
// failed to handleOp. use FailedOp to detect by
6378
if (e.errorName !== 'FailedOp') {
@@ -119,9 +134,9 @@ export class BundleManager {
119134
let validationResult: ValidationResult
120135
try {
121136
// re-validate UserOp. no need to check stake, since it cannot be reduced between first and 2nd validation
122-
validationResult = await this.validationManager.validateUserOp(entry.userOp, false)
137+
validationResult = await this.validationManager.validateUserOp(entry.userOp, entry.referencedContracts, false)
123138
} catch (e: any) {
124-
debug('failed 2nd validation', e.message)
139+
debug('failed 2nd validation:', e.message)
125140
// failed validation. don't try anymore
126141
this.mempoolManager.removeUserOp(entry.userOp)
127142
continue

packages/bundler/src/modules/ExecutionManager.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ReputationManager } from './ReputationManager'
22
import { clearInterval } from 'timers'
33
import { MempoolManager } from './MempoolManager'
4-
import { BundleManager } from './BundleManager'
4+
import { BundleManager, SendBundleReturn } from './BundleManager'
55
import Debug from 'debug'
66
import { UserOperation } from './moduleUtils'
77
import { ValidationManager } from './ValidationManager'
@@ -39,10 +39,11 @@ export class ExecutionManager {
3939
await this.mutex.runExclusive(async () => {
4040
debug('sendUserOperation')
4141
this.validationManager.validateInputParameters(userOp, entryPointInput)
42-
const validationResult = await this.validationManager.validateUserOp(userOp)
42+
const validationResult = await this.validationManager.validateUserOp(userOp, undefined)
4343
this.mempoolManager.addUserOp(userOp,
4444
validationResult.returnInfo.prefund,
4545
validationResult.senderInfo,
46+
validationResult.referencedContracts,
4647
validationResult.aggregatorInfo?.addr)
4748
await this.attemptBundle(false)
4849
})
@@ -80,10 +81,10 @@ export class ExecutionManager {
8081
* attempt to send a bundle now.
8182
* @param force
8283
*/
83-
async attemptBundle (force = true): Promise<void> {
84+
async attemptBundle (force = true): Promise<SendBundleReturn | undefined> {
8485
debug('attemptBundle force=', force, 'count=', this.mempoolManager.count(), 'max=', this.maxMempoolSize)
8586
if (force || this.mempoolManager.count() >= this.maxMempoolSize) {
86-
await this.bundleManager.sendNextBundle()
87+
return await this.bundleManager.sendNextBundle()
8788
}
8889
}
8990
}

packages/bundler/src/modules/MempoolManager.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { BigNumber, BigNumberish } from 'ethers'
22
import { getAddr, UserOperation } from './moduleUtils'
33
import { requireCond } from '../utils'
4-
import { StakeInfo, ValidationErrors } from './ValidationManager'
4+
import { ReferencedCodeHashes, StakeInfo, ValidationErrors } from './ValidationManager'
55
import { ReputationManager } from './ReputationManager'
66
import Debug from 'debug'
77

@@ -10,6 +10,7 @@ const debug = Debug('aa.mempool')
1010
export interface MempoolEntry {
1111
userOp: UserOperation
1212
prefund: BigNumberish
13+
referencedContracts: ReferencedCodeHashes
1314
// aggregator, if one was found during simulation
1415
aggregator?: string
1516
}
@@ -35,10 +36,11 @@ export class MempoolManager {
3536
// add userOp into the mempool, after initial validation.
3637
// replace existing, if any (and if new gas is higher)
3738
// revets if unable to add UserOp to mempool (too many UserOps with this sender)
38-
addUserOp (userOp: UserOperation, prefund: BigNumberish, senderInfo: StakeInfo, aggregator?: string): void {
39+
addUserOp (userOp: UserOperation, prefund: BigNumberish, senderInfo: StakeInfo, referencedContracts: ReferencedCodeHashes, aggregator?: string): void {
3940
const entry: MempoolEntry = {
4041
userOp,
4142
prefund,
43+
referencedContracts,
4244
aggregator
4345
}
4446
const index = this._find(userOp)

0 commit comments

Comments
 (0)