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

EREP-010: paymaster should have deposit to cover all userops #194

Merged
merged 5 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .idea/aa-bundler.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions .idea/runConfigurations/all_bundler_tests.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions packages/bundler/src/modules/DepositManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { BigNumber } from 'ethers'
import { getUserOpMaxCost, IEntryPoint, requireCond, UserOperation, ValidationErrors } from '@account-abstraction/utils'
import { MempoolManager } from './MempoolManager'

/**
* manage paymaster deposits, to make sure a paymaster has enough gas for all its pending transaction in the mempool
* [EREP-010]
*/
export class DepositManager {
deposits: { [addr: string]: BigNumber } = {}

constructor (readonly entryPoint: IEntryPoint, readonly mempool: MempoolManager) {
}

async checkPaymasterDeposit (userOp: UserOperation): Promise<void> {
const paymaster = userOp.paymaster
if (paymaster == null) {
return
}
let deposit = await this.getCachedDeposit(paymaster)
deposit = deposit.sub(getUserOpMaxCost(userOp))

for (const entry of this.mempool.getMempool()) {
if (entry.userOp.paymaster === paymaster) {
deposit =
deposit.sub(BigNumber.from(getUserOpMaxCost(userOp)))
}
}

// [EREP-010] paymaster is required to have balance for all its pending transactions.
// on-chain AA31 checks the deposit for the current userop.
// but submitting all these UserOps it will eventually abort on this error,
// so it's fine to return the same code.
requireCond(deposit.gte(0), 'paymaster deposit too low for all mempool UserOps', ValidationErrors.PaymasterDepositTooLow)
}

/**
* clear deposits after some known change on-chain
*/
clearCache (): void {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it is a bad idea to have the cache stored in DepositManager but trigger the clearing of the cache from the EventsManager. I suggest you keep the entire logic of the cache inside this class, i.e. let the getCachedDeposit() function do the clearCache once every X blocks or whatever.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the DepositManager manages the deposits. the simplest impl is to read from the blockchain everytime.
Instead, I wanted some optimization, and cache it until there is a network change - which is always signaled by a message.

this.deposits = {}
}

async getCachedDeposit (addr: string): Promise<BigNumber> {
let deposit = this.deposits[addr]
if (deposit == null) {
deposit = this.deposits[addr] = await this.entryPoint.balanceOf(addr)
}
return deposit
}
}
9 changes: 8 additions & 1 deletion packages/bundler/src/modules/ExecutionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { clearInterval } from 'timers'
import { BundleManager, SendBundleReturn } from './BundleManager'
import { MempoolManager } from './MempoolManager'
import { ReputationManager } from './ReputationManager'
import { DepositManager } from './DepositManager'

const debug = Debug('aa.exec')

Expand All @@ -24,20 +25,23 @@ export class ExecutionManager {
constructor (private readonly reputationManager: ReputationManager,
private readonly mempoolManager: MempoolManager,
private readonly bundleManager: BundleManager,
private readonly validationManager: ValidationManager
private readonly validationManager: ValidationManager,
private readonly depositManager: DepositManager
) {
}

/**
* send a user operation through the bundler.
* @param userOp the UserOp to send.
* @param entryPointInput the entryPoint passed through the RPC request.
*/
async sendUserOperation (userOp: UserOperation, entryPointInput: string): Promise<void> {
await this.mutex.runExclusive(async () => {
debug('sendUserOperation')
this.validationManager.validateInputParameters(userOp, entryPointInput)
const validationResult = await this.validationManager.validateUserOp(userOp, undefined)
const userOpHash = await this.validationManager.entryPoint.getUserOpHash(packUserOp(userOp))
await this.depositManager.checkPaymasterDeposit(userOp)
this.mempoolManager.addUserOp(userOp,
userOpHash,
validationResult.returnInfo.prefund,
Expand Down Expand Up @@ -90,6 +94,9 @@ export class ExecutionManager {
// in "auto-bundling" mode (which implies auto-mining) also flush mempool from included UserOps
await this.bundleManager.handlePastEvents()
}

this.depositManager.clearCache()

return ret
}
}
Expand Down
8 changes: 6 additions & 2 deletions packages/bundler/src/modules/MempoolManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,15 @@ export class MempoolManager {
this.mempool[index] = entry
} else {
debug('add userOp', userOp.sender, userOp.nonce)
this.checkReputation(senderInfo, paymasterInfo, factoryInfo, aggregatorInfo)
this.checkMultipleRolesViolation(userOp)
this.incrementEntryCount(userOp.sender)
if (userOp.paymaster != null) {
this.incrementEntryCount(userOp.paymaster)
}
if (userOp.factory != null) {
this.incrementEntryCount(userOp.factory)
}
this.checkReputation(senderInfo, paymasterInfo, factoryInfo, aggregatorInfo)
this.checkMultipleRolesViolation(userOp)
this.mempool.push(entry)
}
this.updateSeenStatus(aggregatorInfo?.addr, userOp, senderInfo)
Expand Down Expand Up @@ -290,4 +290,8 @@ export class MempoolManager {

return res.filter(it => it != null).map(it => (it as string).toLowerCase())
}

getMempool (): MempoolEntry[] {
forshtat marked this conversation as resolved.
Show resolved Hide resolved
return this.mempool
}
}
4 changes: 3 additions & 1 deletion packages/bundler/src/modules/initServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { BundlerConfig } from '../BundlerConfig'
import { EventsManager } from './EventsManager'
import { getNetworkProvider } from '../Config'
import { IEntryPoint__factory } from '@account-abstraction/utils'
import { DepositManager } from './DepositManager'

/**
* initialize server modules.
Expand All @@ -21,10 +22,11 @@ export function initServer (config: BundlerConfig, signer: Signer): [ExecutionMa
const reputationManager = new ReputationManager(getNetworkProvider(config.network), BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay)
const mempoolManager = new MempoolManager(reputationManager)
const validationManager = new ValidationManager(entryPoint, config.unsafe)
const depositManager = new DepositManager(entryPoint, mempoolManager)
const eventsManager = new EventsManager(entryPoint, mempoolManager, reputationManager)
const bundleManager = new BundleManager(entryPoint, eventsManager, mempoolManager, validationManager, reputationManager,
config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, config.conditionalRpc)
const executionManager = new ExecutionManager(reputationManager, mempoolManager, bundleManager, validationManager)
const executionManager = new ExecutionManager(reputationManager, mempoolManager, bundleManager, validationManager, depositManager)

reputationManager.addWhitelist(...config.whitelist ?? [])
reputationManager.addBlacklist(...config.blacklist ?? [])
Expand Down
4 changes: 3 additions & 1 deletion packages/bundler/test/BundlerManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { UserOpMethodHandler } from '../src/UserOpMethodHandler'
import { ExecutionManager } from '../src/modules/ExecutionManager'
import { EventsManager } from '../src/modules/EventsManager'
import { createSigner } from './testUtils'
import { DepositManager } from '../src/modules/DepositManager'

describe('#BundlerManager', () => {
let bm: BundleManager
Expand Down Expand Up @@ -102,9 +103,10 @@ describe('#BundlerManager', () => {
const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay)
const mempoolMgr = new MempoolManager(repMgr)
const validMgr = new ValidationManager(_entryPoint, config.unsafe)
const depositManager = new DepositManager(entryPoint, mempoolMgr)
const evMgr = new EventsManager(_entryPoint, mempoolMgr, repMgr)
bundleMgr = new BundleManager(_entryPoint, evMgr, mempoolMgr, validMgr, repMgr, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, false)
const execManager = new ExecutionManager(repMgr, mempoolMgr, bundleMgr, validMgr)
const execManager = new ExecutionManager(repMgr, mempoolMgr, bundleMgr, validMgr, depositManager)
execManager.setAutoBundler(0, 1000)

methodHandler = new UserOpMethodHandler(
Expand Down
4 changes: 3 additions & 1 deletion packages/bundler/test/BundlerServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ExecutionManager } from '../src/modules/ExecutionManager'
import { UserOpMethodHandler } from '../src/UserOpMethodHandler'
import { ethers } from 'hardhat'
import { BundlerConfig } from '../src/BundlerConfig'
import { DepositManager } from '../src/modules/DepositManager'

describe('BundleServer', function () {
let entryPoint: IEntryPoint
Expand Down Expand Up @@ -47,9 +48,10 @@ describe('BundleServer', function () {
const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay)
const mempoolMgr = new MempoolManager(repMgr)
const validMgr = new ValidationManager(entryPoint, config.unsafe)
const depositManager = new DepositManager(entryPoint, mempoolMgr)
const evMgr = new EventsManager(entryPoint, mempoolMgr, repMgr)
const bundleMgr = new BundleManager(entryPoint, evMgr, mempoolMgr, validMgr, repMgr, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, false)
const execManager = new ExecutionManager(repMgr, mempoolMgr, bundleMgr, validMgr)
const execManager = new ExecutionManager(repMgr, mempoolMgr, bundleMgr, validMgr, depositManager)
const methodHandler = new UserOpMethodHandler(
execManager,
provider,
Expand Down
4 changes: 3 additions & 1 deletion packages/bundler/test/DebugMethodHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { expect } from 'chai'
import { createSigner } from './testUtils'
import { EventsManager } from '../src/modules/EventsManager'
import { DepositManager } from '../src/modules/DepositManager'

const provider = ethers.provider

Expand Down Expand Up @@ -56,10 +57,11 @@ describe('#DebugMethodHandler', () => {
const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay)
const mempoolMgr = new MempoolManager(repMgr)
const validMgr = new ValidationManager(entryPoint, config.unsafe)
const depositManager = new DepositManager(entryPoint, mempoolMgr)
const eventsManager = new EventsManager(entryPoint, mempoolMgr, repMgr)
const bundleMgr = new BundleManager(entryPoint, eventsManager, mempoolMgr, validMgr, repMgr,
config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, false)
const execManager = new ExecutionManager(repMgr, mempoolMgr, bundleMgr, validMgr)
const execManager = new ExecutionManager(repMgr, mempoolMgr, bundleMgr, validMgr, depositManager)
methodHandler = new UserOpMethodHandler(
execManager,
provider,
Expand Down
12 changes: 7 additions & 5 deletions packages/bundler/test/UserOpMethodHandler.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BaseProvider } from '@ethersproject/providers'
import { JsonRpcProvider } from '@ethersproject/providers'
import { assert, expect } from 'chai'
import { parseEther, resolveProperties } from 'ethers/lib/utils'

Expand All @@ -21,7 +21,7 @@ import {
packUserOp,
resolveHexlify,
SimpleAccountFactory__factory,
UserOperation,
UserOperation, UserOperationEventEvent,
waitFor
} from '@account-abstraction/utils'
import { UserOperationReceipt } from '../src/RpcTypes'
Expand All @@ -33,13 +33,14 @@ import { UserOpMethodHandler } from '../src/UserOpMethodHandler'
import { ethers } from 'hardhat'
import { createSigner } from './testUtils'
import { EventsManager } from '../src/modules/EventsManager'
import { DepositManager } from '../src/modules/DepositManager'

describe('UserOpMethodHandler', function () {
const helloWorld = 'hello world'

let accountDeployerAddress: string
let methodHandler: UserOpMethodHandler
let provider: BaseProvider
let provider: JsonRpcProvider
let signer: Signer
const accountSigner = Wallet.createRandom()
let mempoolMgr: MempoolManager
Expand Down Expand Up @@ -80,9 +81,10 @@ describe('UserOpMethodHandler', function () {
const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay)
mempoolMgr = new MempoolManager(repMgr)
const validMgr = new ValidationManager(entryPoint, config.unsafe)
const depositManager = new DepositManager(entryPoint, mempoolMgr)
const evMgr = new EventsManager(entryPoint, mempoolMgr, repMgr)
const bundleMgr = new BundleManager(entryPoint, evMgr, mempoolMgr, validMgr, repMgr, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, false)
const execManager = new ExecutionManager(repMgr, mempoolMgr, bundleMgr, validMgr)
const execManager = new ExecutionManager(repMgr, mempoolMgr, bundleMgr, validMgr, depositManager)
methodHandler = new UserOpMethodHandler(
execManager,
provider,
Expand Down Expand Up @@ -193,7 +195,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]))

const transactionReceipt = await event.getTransactionReceipt()
const transactionReceipt = await event!.getTransactionReceipt()
assert.isNotNull(transactionReceipt)
const logs = transactionReceipt.logs.filter(log => log.address === entryPoint.address)
.map(log => entryPoint.interface.parseLog(log))
Expand Down
6 changes: 2 additions & 4 deletions packages/bundler/test/ValidateManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ describe('#ValidationManager', () => {
await testExistingUserOp('balance-self', undefined)
})

it('should fail with unstaked paymaster returning context', async () => {
it('should accept unstaked paymaster returning context', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this change supposed to be included in this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

unrelated. will separate.

const pm = await new TestStorageAccount__factory(ethersSigner).deploy()
// await entryPoint.depositTo(pm.address, { value: parseEther('0.1') })
// await pm.addStake(entryPoint.address, { value: parseEther('0.1') })
Expand All @@ -345,9 +345,7 @@ describe('#ValidationManager', () => {
paymasterPostOpGasLimit: 1e6,
paymasterData: Buffer.from('postOp-context')
}
expect(await vm.validateUserOp(userOp)
.then(() => 'should fail', e => e.message))
.to.match(/unstaked paymaster must not return context/)
await vm.validateUserOp(userOp)
})

it('should fail if validation recursively calls handleOps', async () => {
Expand Down
17 changes: 17 additions & 0 deletions packages/utils/src/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export enum ValidationErrors {
InsufficientStake = -32505,
UnsupportedSignatureAggregator = -32506,
InvalidSignature = -32507,
PaymasterDepositTooLow = -32508,
UserOperationReverted = -32521
}

Expand Down Expand Up @@ -189,3 +190,19 @@ export async function runContractScript<T extends ContractFactory> (provider: Pr
if (parsed == null) throw new Error('unable to parse script (error) response: ' + ret)
return parsed.args
}

/**
* sum the given bignumberish items (numbers, hex, bignumbers)
*/
export function sum (...args: BigNumberish[]): BigNumber {
return args.reduce((acc: BigNumber, cur) => acc.add(cur), BigNumber.from(0))
}

/**
* calculate the maximum verification cost of a UserOperation.
* the cost is the sum of the verification gas limits, multiplied by the maxFeePerGas.
* @param userOp
*/
export function getUserOpMaxCost (userOp: UserOperation): number {
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks like Go code. Shouldn't we make UserOperation a class?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, should pack all userop related utils (resemble UserOperationLib in solidity - but we don't have "using" in TS..)

return sum(userOp.preVerificationGas, userOp.verificationGasLimit, userOp.paymasterVerificationGasLimit ?? 0).mul(userOp.maxFeePerGas).toNumber()
}
3 changes: 3 additions & 0 deletions scripts/check
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

curl -s https://raw.githubusercontent.com/ethereum/ERCs/master/ERCS/erc-7562.md > /tmp/erc-7562.md
`dirname $0`/checkRulesCoverage /tmp/erc-7562.md `dirname $0`/../packages/validation-manager | grep unmatched|grep -v -i -w ALT | grep -v REMOVED
6 changes: 3 additions & 3 deletions scripts/checkRulesCoverage
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
const fs = require('fs')
const {execSync} = require( 'child_process')

args = process.argv.slice(2)
const args = process.argv.slice(2)

let showRev=true;
let showMatch=true;
Expand Down Expand Up @@ -36,7 +36,7 @@ for( let line of fileData.split(/\n/) ) {

line = line.replaceAll( /\*\*(.*?)\*\*/g, '$1' )
const match = line.match(/\[(\w+-\d+)\][\s-:]+\s*([\s\S]*)/);
if ( !match )
if ( !match )
continue;
const [_, rule, rest] = match
rules[rule] = rest
Expand All @@ -58,7 +58,7 @@ function foundRule(r) {
}
}

const grepCmd = execSync( `grep -r . ${folder}`, { maxBuffer: 3e6}).toString()
const grepCmd = execSync( `grep -r . ${folder}`, { maxBuffer: 3e6, encoding:'ascii'})

for ( const line of grepCmd.split(/\n/) ) {
const mm = line.replaceAll( /\b(\w{2,3}-\d+)\b/g, rule=>foundRule(rule))
Expand Down
Loading