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-440 validate using handleOps #243

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[submodule "submodules/account-abstraction"]
path = submodules/account-abstraction
url = https://github.com/eth-infinitism/account-abstraction.git
branch = releases/v0.7
branch = callValidateUserOp-catch-revert
Copy link
Contributor

Choose a reason for hiding this comment

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

This is temporary, no?

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, though the new value will be "develop", until we complete releases/v0.8

[submodule "submodules/rip7560"]
path = submodules/rip7560
url = https://github.com/eth-infinitism/rip7560_contracts.git
3 changes: 3 additions & 0 deletions packages/bundler/src/runBundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ export async function runBundler (argv: string[], overrideExit = true): Promise<
entryPoint
} = await connectContracts(wallet, !config.rip7560)
// bundleSize=1 replicate current immediate bundling mode
if (entryPoint != null && entryPoint.address != null) {
config.entryPoint = entryPoint.address
}
const execManagerConfig = {
...config
// autoBundleMempoolSize: 0
Expand Down
10 changes: 7 additions & 3 deletions packages/bundler/test/BundlerServer.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { JsonRpcProvider } from '@ethersproject/providers'
import { ethers } from 'hardhat'
import { expect } from 'chai'
import { parseEther } from 'ethers/lib/utils'

Expand Down Expand Up @@ -29,9 +28,14 @@ describe('BundleServer', function () {
let entryPoint: IEntryPoint
let server: BundlerServer
before(async () => {
const provider = ethers.provider
// const provider = ethers.provider
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove

const provider = new JsonRpcProvider('http://localhost:8545')
const signer = await createSigner()
entryPoint = await deployEntryPoint(provider)
try {
entryPoint = await deployEntryPoint(provider)
} catch (e) {
throw new Error('Failed to deploy entry point - no geth?\n' + e)
Copy link
Contributor

Choose a reason for hiding this comment

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

Geth or just a node?

}

const config: BundlerConfig = {
chainId: 1337,
Expand Down
20 changes: 13 additions & 7 deletions packages/sdk/src/BaseAccountAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ethers, BigNumber, BigNumberish, BytesLike } from 'ethers'
import { Provider } from '@ethersproject/providers'

import { TransactionDetailsForUserOp } from './TransactionDetailsForUserOp'
import { defaultAbiCoder } from 'ethers/lib/utils'
import { defaultAbiCoder, hexConcat, hexDataSlice } from 'ethers/lib/utils'
import { PaymasterAPI } from './PaymasterAPI'
import { encodeUserOp, getUserOpHash, IEntryPoint, IEntryPoint__factory, UserOperation } from '@account-abstraction/utils'

Expand Down Expand Up @@ -120,12 +120,13 @@ export abstract class BaseAccountAPI {
if (factory == null) {
throw new Error(('no counter factual address if not factory'))
}
// use entryPoint to query account address (factory can provide a helper method to do the same, but
// this method attempts to be generic
const retAddr = await this.provider.call({
to: factory, data: factoryData
const getSenderAddressData = this.entryPointView.interface.encodeFunctionData('getSenderAddress', [hexConcat([factory, factoryData ?? '0x'])])
Copy link
Contributor

@forshtat forshtat Feb 13, 2025

Choose a reason for hiding this comment

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

Why don't we use to the factory contract object here? Manually encoding and decoding this data is not pretty.

const senderAddressResult = await this.provider.call({
to: this.entryPointAddress,
data: getSenderAddressData
})
const [addr] = defaultAbiCoder.decode(['address'], retAddr)
// the result is "error SenderAddressResult(address)", so remove methodsig first.
const [addr] = defaultAbiCoder.decode(['address'], hexDataSlice(senderAddressResult, 4))
return addr
}

Expand Down Expand Up @@ -205,7 +206,12 @@ export abstract class BaseAccountAPI {
if (factoryParams == null) {
return 0
}
return await this.provider.estimateGas({ to: factoryParams.factory, data: factoryParams.factoryData })
const senderCreator = await this.entryPointView.senderCreator()
return await this.provider.estimateGas({
from: senderCreator,
to: factoryParams.factory,
data: factoryParams.factoryData
})
}

/**
Expand Down
5 changes: 4 additions & 1 deletion packages/sdk/src/SimpleAccountAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { arrayify } from 'ethers/lib/utils'
import { Signer } from '@ethersproject/abstract-signer'
import { BaseApiParams, BaseAccountAPI, FactoryParams } from './BaseAccountAPI'
import { ecsign, toRpcSig } from 'ethereumjs-util'

/**
* constructor params, added no top of base params:
Expand Down Expand Up @@ -100,6 +101,8 @@ export class SimpleAccountAPI extends BaseAccountAPI {
}

async signUserOpHash (userOpHash: string): Promise<string> {
return await this.owner.signMessage(arrayify(userOpHash))
const privateKey = (this.owner as any).privateKey
const sig = ecsign(Buffer.from(arrayify(userOpHash)), Buffer.from(arrayify(privateKey)))
return toRpcSig(sig.v, sig.r, sig.s)
Comment on lines +104 to +106
Copy link
Contributor

Choose a reason for hiding this comment

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

Why was this change made and how is it related to this issue?

}
}
31 changes: 31 additions & 0 deletions packages/utils/contracts/GetStakes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
pragma solidity >=0.8;
// SPDX-License-Identifier: GPL-3.0

import "@account-abstraction/contracts/interfaces/IEntryPoint.sol";

struct StakeInfo {
address addr;
uint256 stake;
uint256 unstakeDelaySec;
}

error StakesRet(StakeInfo[] stakes);

// helper: get stake info of multiple entities.
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't we use OpenZeppelin's Multicall contract here instead of reinventing the wheel?
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Multicall.sol

Copy link
Contributor Author

Choose a reason for hiding this comment

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

multicall has to be deployed, and only then can be called.
This contract never gets deployed (in fact, it can't, since it reverts in its constructor)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, but also this is a bit of reinventing the wheel. Maybe we should just give in and deploy the Multicall the same way we deploy EntryPoint? On real networks with real bundlers, it will just be an address config parameter.

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, we could use multicall, but:

  1. need to deploy it, at least once
  2. it loses typechecking. I return an array of structs.

// This contract is never deployed: it is called using eth_call, and it reverts with the result...
contract GetStakes {

constructor(IEntryPoint entryPoint, address[] memory addrs) {
StakeInfo[] memory stakes = getStakes(entryPoint, addrs);
revert StakesRet(stakes);
}

function getStakes(IEntryPoint entryPoint, address[] memory addrs) public view returns (StakeInfo[] memory) {
StakeInfo[] memory stakes = new StakeInfo[](addrs.length);
for (uint256 i = 0; i < addrs.length; i++) {
IStakeManager.DepositInfo memory info = entryPoint.getDepositInfo(addrs[i]);
stakes[i] = StakeInfo(addrs[i], info.stake, info.unstakeDelaySec);
}
return stakes;
}
}
4 changes: 2 additions & 2 deletions packages/utils/contracts/Imports.sol
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;
pragma solidity ^0.8;

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 "@account-abstraction/contracts/samples/TokenPaymaster.sol";
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove

import "@account-abstraction/rip7560/contracts/predeploys/Rip7560StakeManager.sol";

import {NonceManager as NonceManagerRIP7712} from "@account-abstraction/rip7560/contracts/predeploys/NonceManager.sol";
3 changes: 2 additions & 1 deletion packages/utils/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ const config: HardhatUserConfig = {
target: 'ethers-v5'
},
solidity: {
version: '0.8.23',
version: '0.8.28',
settings: {
evmVersion: 'cancun',
optimizer: { enabled: true }
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"@ethersproject/abstract-signer": "^5.7.0",
"@ethersproject/keccak256": "^5.7.0",
"@ethersproject/providers": "^5.7.0",
"@openzeppelin/contracts": "^5.0.1",
"@openzeppelin/contracts": "^5.2.0",
"debug": "^4.3.4",
"ethereumjs-util": "^7.1.5",
"ethers": "^5.7.0",
Expand Down
69 changes: 58 additions & 11 deletions packages/utils/src/ERC4337Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from 'ethers/lib/utils'
import { abi as entryPointAbi } from '@account-abstraction/contracts/artifacts/IEntryPoint.json'

import { BigNumber, BigNumberish, BytesLike, ethers } from 'ethers'
import { BigNumber, BigNumberish, BytesLike, ethers, TypedDataDomain, TypedDataField } from 'ethers'
import Debug from 'debug'
import { PackedUserOperation } from './Utils'
import { UserOperation } from './interfaces/UserOperation'
Expand All @@ -25,6 +25,13 @@ if (PackedUserOpType == null) {

export const AddressZero = ethers.constants.AddressZero

// Matched to domain name, version from EntryPoint.sol:
const DOMAIN_NAME = 'ERC4337'
const DOMAIN_VERSION = '1'

// Matched to UserOperationLib.sol:
const PACKED_USEROP_TYPEHASH = keccak256(Buffer.from('PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData)'))

// reverse "Deferrable" or "PromiseOrValue" fields
export type NotPromise<T> = {
[P in keyof T]: Exclude<T[P], Promise<any>>
Expand Down Expand Up @@ -214,19 +221,21 @@ export function encodeUserOp (op1: PackedUserOperation | UserOperation, forSigna
}
if (forSignature) {
return defaultAbiCoder.encode(
['address', 'uint256', 'bytes32', 'bytes32',
['bytes32', 'address', 'uint256', 'bytes32', 'bytes32',
'bytes32', 'uint256', 'bytes32',
'bytes32'],
[op.sender, op.nonce, keccak256(op.initCode), keccak256(op.callData),
[PACKED_USEROP_TYPEHASH,
op.sender, op.nonce, keccak256(op.initCode), keccak256(op.callData),
op.accountGasLimits, op.preVerificationGas, op.gasFees,
keccak256(op.paymasterAndData)])
} else {
// for the purpose of calculating gas cost encode also signature (and no keccak of bytes)
return defaultAbiCoder.encode(
['address', 'uint256', 'bytes', 'bytes',
['bytes32', 'address', 'uint256', 'bytes', 'bytes',
'bytes32', 'uint256', 'bytes32',
'bytes', 'bytes'],
[op.sender, op.nonce, op.initCode, op.callData,
[PACKED_USEROP_TYPEHASH,
op.sender, op.nonce, op.initCode, op.callData,
op.accountGasLimits, op.preVerificationGas, op.gasFees,
op.paymasterAndData, op.signature])
}
Expand All @@ -242,14 +251,52 @@ export function encodeUserOp (op1: PackedUserOperation | UserOperation, forSigna
* @param chainId
*/
export function getUserOpHash (op: UserOperation, entryPoint: string, chainId: number): string {
const userOpHash = keccak256(encodeUserOp(op, true))
const enc = defaultAbiCoder.encode(
['bytes32', 'address', 'uint256'],
[userOpHash, entryPoint, chainId])
return keccak256(enc)
const packed = encodeUserOp(op, true)
return keccak256(hexConcat([
'0x1901',
getDomainSeparator(entryPoint, chainId),
keccak256(packed)
]))
}

export function getDomainSeparator (entryPoint: string, chainId: number): string {
const domainData = getErc4337TypedDataDomain(entryPoint, chainId) as Required<TypedDataDomain>
return keccak256(defaultAbiCoder.encode(
['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'],
[
keccak256(Buffer.from('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')),
keccak256(Buffer.from(domainData.name)),
keccak256(Buffer.from(domainData.version)),
domainData.chainId,
domainData.verifyingContract
]))
}

export function getErc4337TypedDataDomain (entryPoint: string, chainId: number): TypedDataDomain {
return {
name: DOMAIN_NAME,
version: DOMAIN_VERSION,
chainId,
verifyingContract: entryPoint
}
}

export function getErc4337TypedDataTypes (): { [type: string]: TypedDataField[] } {
return {
PackedUserOperation: [
{ name: 'sender', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'initCode', type: 'bytes' },
{ name: 'callData', type: 'bytes' },
{ name: 'accountGasLimits', type: 'bytes32' },
{ name: 'preVerificationGas', type: 'uint256' },
{ name: 'gasFees', type: 'bytes32' },
{ name: 'paymasterAndData', type: 'bytes' }
]
}
}

const ErrorSig = keccak256(Buffer.from('Error(string)')).slice(0, 10) // 0x08c379a0
export const ErrorSig = keccak256(Buffer.from('Error(string)')).slice(0, 10) // 0x08c379a0
const FailedOpSig = keccak256(Buffer.from('FailedOp(uint256,string)')).slice(0, 10) // 0x220266b6

interface DecodedError {
Expand Down
6 changes: 3 additions & 3 deletions packages/utils/src/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,10 @@ export async function runContractScript<T extends ContractFactory> (provider: Pr
}

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

/**
Expand Down
3 changes: 1 addition & 2 deletions packages/utils/src/deployStakeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { JsonRpcProvider } from '@ethersproject/providers'

import { bytecode as stakeManagerByteCode } from '../artifacts/@account-abstraction/rip7560/contracts/predeploys/Rip7560StakeManager.sol/Rip7560StakeManager.json'
import { DeterministicDeployer } from './DeterministicDeployer'
import { Rip7560StakeManager__factory } from './types/factories/@account-abstraction/rip7560/contracts/predeploys'
import { Rip7560StakeManager } from './types/@account-abstraction/rip7560/contracts/predeploys'
import { Rip7560StakeManager__factory, Rip7560StakeManager } from './types'

export const stakeManagerSalt = '0x90d8084deab30c2a37c45e8d47f49f2f7965183cb6990a98943ef94940681de3'

Expand Down
3 changes: 2 additions & 1 deletion packages/utils/src/interfaces/EIP7702Authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import RLP from 'rlp'
import { bytesToHex, ecrecover, hexToBigInt, hexToBytes, PrefixedHexString, pubToAddress } from '@ethereumjs/util'
import { AddressZero } from '../ERC4337Utils'
import { keccak256 } from '@ethersproject/keccak256'
import { toChecksumAddress } from 'ethereumjs-util'

export interface EIP7702Authorization {
chainId: BigNumberish
Expand Down Expand Up @@ -39,7 +40,7 @@ export function getEip7702AuthorizationSigner (authorization: EIP7702Authorizati
// eslint-disable-next-line @typescript-eslint/no-base-to-string
hexToBytes(authorization.s.toString() as `0x${string}`)
)
const sender = bytesToHex(pubToAddress(senderPubKey))
const sender = toChecksumAddress(bytesToHex(pubToAddress(senderPubKey)))
if (sender === AddressZero) {
throw new Error(`Failed to recover authorization for address ${authorization.address}`)
}
Expand Down
1 change: 1 addition & 0 deletions packages/validation-manager/src/GethTracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export interface TraceOptions {
tracer?: LogTracerFunc | string // Setting this will enable JavaScript-based transaction tracing, described below. If set, the previous four arguments will be ignored.
timeout?: string // Overrides the default timeout of 5 seconds for JavaScript-based tracing calls. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
stateOverrides?: any
blockOverrides?: any
}

// the result type of debug_traceCall and debug_traceTransaction
Expand Down
Loading
Loading