Skip to content

Commit

Permalink
AA-412: Implement RIP-7712 NonceManager (#221)
Browse files Browse the repository at this point in the history
* Add 'rip7560_contracts' as a submodule

* add 'rip7560_contracts' compilation to the build process

* Add 'BigNonce' field support

* Separate 'nonce' and 'bigNonce' fields

* trigger 'handlePastEvents' manually to clear mempool

* Introduce 'nonceKey' in RIP-7560 transaction type
  • Loading branch information
forshtat authored Aug 19, 2024
1 parent 41e1831 commit f54bce2
Show file tree
Hide file tree
Showing 18 changed files with 735 additions and 34 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
path = submodules/account-abstraction
url = https://github.com/eth-infinitism/account-abstraction.git
branch = releases/v0.7
[submodule "submodules/rip7560"]
path = submodules/rip7560
url = https://github.com/eth-infinitism/rip7560_contracts.git
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"workspaces": {
"packages": [
"packages/*",
"submodules/account-abstraction/contracts"
"submodules/account-abstraction/contracts",
"submodules/rip7560"
],
"nohoist": [
"**eslint**"
Expand Down
6 changes: 3 additions & 3 deletions packages/bundler/src/MethodHandlerRIP7560.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import {
OperationBase,
OperationRIP7560,
StorageMap,
getPackedNonce,
getRIP7560TransactionHash,
requireCond,
tostr
requireCond
} from '@account-abstraction/utils'
import { ExecutionManager } from './modules/ExecutionManager'
import { HEX_REGEX } from './MethodHandlerERC4337'
Expand All @@ -24,7 +24,7 @@ export class MethodHandlerRIP7560 {

async sendRIP7560Transaction (transaction: OperationRIP7560): Promise<string> {
await this._validateParameters(transaction)
console.log(`RIP7560Transaction: Sender=${transaction.sender} Nonce=${tostr(transaction.nonce)} Paymaster=${transaction.paymaster ?? ''}`)
console.log(`RIP7560Transaction: Sender=${transaction.sender} Nonce=${getPackedNonce(transaction).toHexString()} Paymaster=${transaction.paymaster ?? ''}`)
await this.execManager.sendUserOperation(transaction, '')
return getRIP7560TransactionHash(transaction)
}
Expand Down
6 changes: 3 additions & 3 deletions packages/bundler/src/modules/BundleManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,16 +247,16 @@ export class BundleManager implements IBundleManager {
}
// [GREP-020] - renamed from [SREP-030]
if (paymaster != null && (paymasterStatus === ReputationStatus.THROTTLED ?? (stakedEntityCount[paymaster] ?? 0) > THROTTLED_ENTITY_BUNDLE_COUNT)) {
debug('skipping throttled paymaster', entry.userOp.sender, entry.userOp.nonce)
debug('skipping throttled paymaster', entry.userOp.sender, (entry.userOp as any).nonce)
continue
}
// [GREP-020] - renamed from [SREP-030]
if (factory != null && (deployerStatus === ReputationStatus.THROTTLED ?? (stakedEntityCount[factory] ?? 0) > THROTTLED_ENTITY_BUNDLE_COUNT)) {
debug('skipping throttled factory', entry.userOp.sender, entry.userOp.nonce)
debug('skipping throttled factory', entry.userOp.sender, (entry.userOp as any).nonce)
continue
}
if (senders.has(entry.userOp.sender)) {
debug('skipping already included sender', entry.userOp.sender, entry.userOp.nonce)
debug('skipping already included sender', entry.userOp.sender, (entry.userOp as any).nonce)
// allow only a single UserOp per sender per bundle
continue
}
Expand Down
1 change: 1 addition & 0 deletions packages/bundler/src/modules/ExecutionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export class ExecutionManager {
}
await new Promise(resolve => setTimeout(resolve, 50))
}
await this.bundleManager.handlePastEvents()
return
}
debug('attemptBundle force=', force, 'count=', this.mempoolManager.count(), 'max=', this.maxMempoolSize)
Expand Down
20 changes: 13 additions & 7 deletions packages/bundler/src/modules/MempoolManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { BigNumber, BigNumberish } from 'ethers'
import Debug from 'debug'

import {
OperationBase,
ReferencedCodeHashes,
RpcError,
StakeInfo,
ValidationErrors,
requireCond, OperationBase
getPackedNonce,
requireCond
} from '@account-abstraction/utils'
import { MempoolEntry } from './MempoolEntry'
import { ReputationManager } from './ReputationManager'
Expand Down Expand Up @@ -75,14 +77,15 @@ export class MempoolManager {
referencedContracts,
aggregatorInfo?.addr
)
const index = this._findBySenderNonce(userOp.sender, userOp.nonce)
const packedNonce = getPackedNonce(entry.userOp)
const index = this._findBySenderNonce(userOp.sender, packedNonce)
if (index !== -1) {
const oldEntry = this.mempool[index]
this.checkReplaceUserOp(oldEntry, entry)
debug('replace userOp', userOp.sender, userOp.nonce)
debug('replace userOp', userOp.sender, packedNonce)
this.mempool[index] = entry
} else {
debug('add userOp', userOp.sender, userOp.nonce)
debug('add userOp', userOp.sender, packedNonce)
this.checkReputation(senderInfo, paymasterInfo, factoryInfo, aggregatorInfo)
this.checkMultipleRolesViolation(userOp)
this.incrementEntryCount(userOp.sender)
Expand Down Expand Up @@ -196,7 +199,8 @@ export class MempoolManager {
_findBySenderNonce (sender: string, nonce: BigNumberish): number {
for (let i = 0; i < this.mempool.length; i++) {
const curOp = this.mempool[i].userOp
if (curOp.sender === sender && curOp.nonce === nonce) {
const packedNonce = getPackedNonce(curOp)
if (curOp.sender === sender && packedNonce.eq(nonce)) {
return i
}
}
Expand All @@ -222,11 +226,13 @@ export class MempoolManager {
if (typeof userOpOrHash === 'string') {
index = this._findByHash(userOpOrHash)
} else {
index = this._findBySenderNonce(userOpOrHash.sender, userOpOrHash.nonce)
const packedNonce = getPackedNonce(userOpOrHash)
index = this._findBySenderNonce(userOpOrHash.sender, packedNonce)
}
if (index !== -1) {
const userOp = this.mempool[index].userOp
debug('removeUserOp', userOp.sender, userOp.nonce)
const packedNonce = getPackedNonce(userOp)
debug('removeUserOp', userOp.sender, packedNonce)
this.mempool.splice(index, 1)
this.decrementEntryCount(userOp.sender)
this.decrementEntryCount(userOp.paymaster)
Expand Down
5 changes: 5 additions & 0 deletions packages/bundler/src/runBundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { bundlerConfigDefault } from './BundlerConfig'
import { parseEther } from 'ethers/lib/utils'
import { MethodHandlerRIP7560 } from './MethodHandlerRIP7560'
import { JsonRpcProvider } from '@ethersproject/providers'
import { deployNonceManager } from '@account-abstraction/utils/dist/src/RIP7712NonceManagerUtils'

// this is done so that console.log outputs BigNumber as hex string instead of unreadable object
export const inspectCustomSymbol = Symbol.for('nodejs.util.inspect.custom')
Expand Down Expand Up @@ -134,6 +135,10 @@ export async function runBundler (argv: string[], overrideExit = true): Promise<
process.exit(1)
}

if (config.rip7560) {
await deployNonceManager(provider, wallet as any)
}

const {
entryPoint
} = await connectContracts(wallet, !config.rip7560)
Expand Down
1 change: 1 addition & 0 deletions packages/bundler/test/RIP7560Mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe.skip('RIP7560Mode', function () {
// we stub the ValidationManagerRIP7560 so the values of these fields do not matter
operationRIP7560 = {
accessList: undefined,
nonceKey: '0x0',
nonce: '0x0',
sender: '0x0000000000000000000000000000000000000000',
callGasLimit: '0x0',
Expand Down
2 changes: 2 additions & 0 deletions packages/utils/contracts/Imports.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ import "@account-abstraction/contracts/core/EntryPointSimulations.sol";
import "@account-abstraction/contracts/interfaces/IStakeManager.sol";
import "@account-abstraction/contracts/samples/SimpleAccountFactory.sol";
import "@account-abstraction/contracts/samples/TokenPaymaster.sol";

import {NonceManager as NonceManagerRIP7712} from "@account-abstraction/rip7560/contracts/predeploys/NonceManager.sol";
3 changes: 2 additions & 1 deletion packages/utils/src/RIP7560Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ function nonZeroAddr(addr?: string): Buffer {
function rlpEncodeRip7560Tx (op: OperationRIP7560, forSignature = true): string {
const input: Input = []
input.push(bigNumberishToUnpaddedBuffer(op.chainId))
input.push(bigNumberishToUnpaddedBuffer(op.nonce))
input.push(bigNumberishToUnpaddedBuffer(op.maxPriorityFeePerGas))
input.push(bigNumberishToUnpaddedBuffer(op.maxFeePerGas))
input.push(bigNumberishToUnpaddedBuffer(op.callGasLimit))
Expand All @@ -40,8 +41,8 @@ function rlpEncodeRip7560Tx (op: OperationRIP7560, forSignature = true): string
input.push(bigNumberishToUnpaddedBuffer(op.verificationGasLimit))
input.push(bigNumberishToUnpaddedBuffer(op.paymasterVerificationGasLimit ?? 0))
input.push(bigNumberishToUnpaddedBuffer(op.paymasterPostOpGasLimit ?? 0))
input.push(bigNumberishToUnpaddedBuffer(op.nonceKey))
input.push(nonZeroAddr(undefined)) // to
input.push(bigNumberishToUnpaddedBuffer(op.nonce))
input.push(bigNumberishToUnpaddedBuffer(0)) // value
let rlpEncoded: any = encode(input)
rlpEncoded = Buffer.from([4, ...rlpEncoded])
Expand Down
16 changes: 16 additions & 0 deletions packages/utils/src/RIP7712NonceManagerUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { JsonRpcProvider } from '@ethersproject/providers'

import { bytecode as nonceManagerByteCode } from '../artifacts/@account-abstraction/rip7560/contracts/predeploys/NonceManager.sol/NonceManager.json'
import { DeterministicDeployer } from './DeterministicDeployer'
import { NonceManager__factory } from './types/factories/@account-abstraction/rip7560/contracts/predeploys'

export const entryPointSalt = '0x90d8084deab30c2a37c45e8d47f49f2f7965183cb6990a98943ef94940681de3'

export async function deployNonceManager (provider: JsonRpcProvider, signer = provider.getSigner()): Promise<any> {
const addr = await new DeterministicDeployer(provider, signer).deterministicDeploy(nonceManagerByteCode, entryPointSalt)
return NonceManager__factory.connect(addr, signer)
}

export function getNonceManagerAddress (): string {
return DeterministicDeployer.getAddress(nonceManagerByteCode, entryPointSalt)
}
16 changes: 14 additions & 2 deletions packages/utils/src/Utils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// misc utilities for the various modules.

import { BytesLike, ContractFactory, BigNumber } from 'ethers'
import { hexlify, hexZeroPad, Result } from 'ethers/lib/utils'
import { BytesLike, ContractFactory, BigNumber, ethers } from 'ethers'
import { defaultAbiCoder, hexlify, hexZeroPad, Result } from 'ethers/lib/utils'
import { Provider, JsonRpcProvider } from '@ethersproject/providers'
import { BigNumberish } from 'ethers/lib/ethers'

import { NotPromise } from './ERC4337Utils'
import { PackedUserOperationStruct } from './soltypes'
import { UserOperation } from './interfaces/UserOperation'
import { OperationBase } from './interfaces/OperationBase'
import { OperationRIP7560 } from './interfaces/OperationRIP7560'

export interface SlotMap {
[slot: string]: string
Expand Down Expand Up @@ -218,3 +219,14 @@ export function getUserOpMaxCost (userOp: OperationBase): BigNumber {
const preVerificationGas: BigNumberish = (userOp as UserOperation).preVerificationGas
return sum(preVerificationGas ?? 0, userOp.verificationGasLimit, userOp.callGasLimit, userOp.paymasterVerificationGasLimit ?? 0, userOp.paymasterPostOpGasLimit ?? 0).mul(userOp.maxFeePerGas)
}

export function getPackedNonce (userOp: OperationBase): BigNumber {
const nonceKey = (userOp as OperationRIP7560).nonceKey
if (nonceKey == null || BigNumber.from(nonceKey).eq(0)) {
// Either not RIP-7560 operation or not using RIP-7712 nonce
return BigNumber.from(userOp.nonce)
}
const packed = ethers.utils.solidityPack(["uint192", "uint64"], [nonceKey, userOp.nonce])
const bigNumberNonce = BigNumber.from(packed)
return bigNumberNonce
}
2 changes: 2 additions & 0 deletions packages/utils/src/interfaces/OperationRIP7560.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ export interface OperationRIP7560 extends OperationBase {
accessList: any
value: BigNumberish
builderFee: BigNumberish
// todo: we discussed using 'nonceKey' in the JSON schema for ERC-4337 as well but we did not finalize this decision
nonceKey: BigNumberish
}
1 change: 1 addition & 0 deletions packages/utils/src/interfaces/UserOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ import { BigNumberish } from 'ethers'
import { OperationBase } from './OperationBase'

export interface UserOperation extends OperationBase {
nonce: BigNumberish
preVerificationGas: BigNumberish
}
3 changes: 2 additions & 1 deletion packages/utils/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"include": [
"./src/**/*.ts"
"./src/**/*.ts",
"./artifacts/**/*.json"
],
"compilerOptions": {
"outDir": "dist",
Expand Down
1 change: 1 addition & 0 deletions packages/validation-manager/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export async function supportsDebugTraceCall (provider: JsonRpcProvider, rip7560
chainId: '0x539',
value: '0x0',
sender: AddressZero,
nonceKey: '0x0',
nonce: '0x0',
callData: '0x',
callGasLimit: '0x0',
Expand Down
1 change: 1 addition & 0 deletions submodules/rip7560
Submodule rip7560 added at 370324
Loading

0 comments on commit f54bce2

Please sign in to comment.