Skip to content

Commit

Permalink
Fix E2E Tests For ERC-4337 v0.7 Bundler (#300)
Browse files Browse the repository at this point in the history
Fixes #227 

This PR updates the E2E tests to work with the latest bundler for the
EntryPoint contract v0.7. Note that there was a notable change in that
the `PackedUserOperation` struct is only used on-chain, and the bundler
RPC uses a different `UserOperation` type that is completely unpacked.
This required some additional helper methods as well as some adjustments
to tests to distinguish between the packed and unpacked versions of user
operations.

Also, unfortunately the reference bundler repository is not tagged yet -
so we are using a commit hash that corresponds to the latest version. I
asked the 4337 team to create a new tag, at which point, I will update
the docker file. Additionally, the latest bundler version uses
submodules, so some dockerfile tweaking was required.

Note that the broken CI is expected until #270 is fixed.
  • Loading branch information
nlordell authored Mar 5, 2024
1 parent 5e21fbc commit f3129de
Show file tree
Hide file tree
Showing 17 changed files with 209 additions and 83 deletions.
4 changes: 3 additions & 1 deletion modules/4337/docker/bundler/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
FROM docker.io/library/node:18

ARG TAG=v0.6.1
# v0.7.0
ARG TAG=26e4f4c
RUN git clone https://github.com/eth-infinitism/bundler /src/bundler
WORKDIR /src/bundler
RUN git checkout ${TAG}
RUN git submodule init && git submodule update

RUN yarn && yarn preprocess
ENTRYPOINT ["yarn", "bundler"]
Expand Down
23 changes: 12 additions & 11 deletions modules/4337/scripts/runOp.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Result } from 'ethers'
import { ethers } from 'hardhat'

import { UserOperation, getRequiredPrefund, getSupportedEntryPoints } from '../src/utils/userOp'
import { getRequiredPrefund, getSupportedEntryPoints } from '../src/utils/userOp'
import { chainId } from '../test/utils/encoding'
import { getSafe4337Module } from '../test/utils/setup'
import { GlobalConfig, MultiProvider4337, Safe4337 } from '../src/utils/safe'
Expand Down Expand Up @@ -108,6 +108,7 @@ const runOp = async () => {
]),
)

const packedUserOp = await operation.packedUserOperation()
console.log(
'validateUserOp',
await ethers.provider.send('eth_call', [
Expand All @@ -116,18 +117,18 @@ const runOp = async () => {
to: safe.address,
data: buildData('validateUserOp((address,uint256,bytes,bytes,bytes32,uint256,bytes32,bytes,bytes),bytes32,uint256)', [
[
userOp.sender,
userOp.nonce,
userOp.initCode,
userOp.callData,
userOp.accountGasLimits,
userOp.preVerificationGas,
userOp.gasFees,
userOp.paymasterAndData,
userOp.signature,
packedUserOp.sender,
packedUserOp.nonce,
packedUserOp.initCode,
packedUserOp.callData,
packedUserOp.accountGasLimits,
packedUserOp.preVerificationGas,
packedUserOp.gasFees,
packedUserOp.paymasterAndData,
packedUserOp.signature,
],
'0x0000000000000000000000000000000000000000000000000000000000000000',
getRequiredPrefund(userOp),
getRequiredPrefund(packedUserOp),
]),
},
'latest',
Expand Down
33 changes: 28 additions & 5 deletions modules/4337/src/utils/safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AddressLike, JsonRpcProvider, Provider, Signer, ethers } from 'ethers'

// Import from Safe contracts repo once it is upgraded to ethers v6 and can be installed via npm
import { MetaTransaction, SafeSignature, SignedSafeTransaction, buildSignatureBytes } from './execution'
import { UserOperation, EIP712_SAFE_OPERATION_TYPE, packGasParameters } from './userOp'
import { PackedUserOperation, UserOperation, EIP712_SAFE_OPERATION_TYPE, packGasParameters, unpackUserOperation } from './userOp'

const AddressOne = '0x0000000000000000000000000000000000000001'

Expand Down Expand Up @@ -31,7 +31,6 @@ export interface OperationParams {
preVerificationGas: bigint
maxPriorityFeePerGas: bigint
maxFeePerGas: bigint
paymasterAndData: string
validAfter: bigint
validUntil: bigint
}
Expand Down Expand Up @@ -122,7 +121,28 @@ export class MultiProvider4337 extends JsonRpcProvider {
}

public async sendUserOperation(userOp: UserOperation, entryPoint: AddressLike): Promise<string> {
return await super.send('eth_sendUserOperation', [userOp, await ethers.resolveAddress(entryPoint, this)])
const jsonUserOp = {
sender: ethers.getAddress(userOp.sender),
nonce: ethers.toBeHex(userOp.nonce),
callData: ethers.hexlify(userOp.callData),
callGasLimit: ethers.toBeHex(userOp.callGasLimit),
verificationGasLimit: ethers.toBeHex(userOp.verificationGasLimit),
preVerificationGas: ethers.toBeHex(userOp.preVerificationGas),
maxFeePerGas: ethers.toBeHex(userOp.maxFeePerGas),
maxPriorityFeePerGas: ethers.toBeHex(userOp.maxPriorityFeePerGas),
signature: ethers.hexlify(userOp.signature),
} as Record<string, unknown>
if (userOp.factory) {
jsonUserOp.factory = ethers.getAddress(userOp.factory)
jsonUserOp.factoryData = ethers.hexlify(userOp.factoryData!)
}
if (userOp.paymaster) {
jsonUserOp.paymaster = ethers.getAddress(userOp.paymaster)
jsonUserOp.paymasterVerificationGasLimit = ethers.toBeHex(userOp.paymasterVerificationGasLimit!)
jsonUserOp.paymasterPostOpGasLimit = ethers.toBeHex(userOp.paymasterPostOpGasLimit!)
jsonUserOp.paymasterData = ethers.hexlify(userOp.paymasterData!)
}
return await super.send('eth_sendUserOperation', [jsonUserOp, await ethers.resolveAddress(entryPoint, this)])
}
}

Expand Down Expand Up @@ -157,7 +177,7 @@ export class Safe4337Operation {
return buildSignatureBytes(this.signatures)
}

async userOperation(paymasterAndData = '0x'): Promise<UserOperation> {
async packedUserOperation(paymasterAndData = '0x'): Promise<PackedUserOperation> {
const { accountGasLimits, gasFees } = packGasParameters(this.params)
return {
nonce: ethers.toBeHex(this.params.nonce),
Expand All @@ -175,6 +195,10 @@ export class Safe4337Operation {
}
}

async userOperation(paymasterAndData = '0x'): Promise<UserOperation> {
return await unpackUserOperation(await this.packedUserOperation(paymasterAndData))
}

async authorize(signer: ethers.Signer) {
const validSigners = await this.safe.getSigners()
const signerAddress = await signer.getAddress()
Expand Down Expand Up @@ -243,7 +267,6 @@ export class Safe4337Operation {
callGasLimit: (BigInt(estimates.callGasLimit) * 12n) / 10n,
validAfter: 0n,
validUntil: 0n,
paymasterAndData: '0x',
}
return new Safe4337Operation(safe, action, params, globalConfig)
}
Expand Down
95 changes: 88 additions & 7 deletions modules/4337/src/utils/userOp.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { BigNumberish, BytesLike, Contract, Signer, ethers } from 'ethers'
import { PackedUserOperationStruct as UserOperation } from '../../typechain-types/contracts/Safe4337Module'
import { PackedUserOperationStruct as PackedUserOperation } from '../../typechain-types/contracts/Safe4337Module'
import { SafeSignature } from './execution'

export { UserOperation }
export { PackedUserOperation }

type OptionalExceptFor<T, TRequired extends keyof T = keyof T> = Partial<Pick<T, Exclude<keyof T, TRequired>>> &
Required<Pick<T, TRequired>>
Expand All @@ -13,7 +13,25 @@ export type SafeUserOperation = {
validAfter: BigNumberish
validUntil: BigNumberish
} & GasParameters &
Omit<UserOperation, 'sender' | 'signature' | keyof PackedGasParameters>
Omit<PackedUserOperation, 'sender' | 'signature' | keyof PackedGasParameters>

export type UserOperation = {
sender: string
nonce: BigNumberish
factory?: string
factoryData?: BytesLike
callData: BytesLike
callGasLimit: BigNumberish
verificationGasLimit: BigNumberish
preVerificationGas: BigNumberish
maxFeePerGas: BigNumberish
maxPriorityFeePerGas: BigNumberish
paymaster?: string
paymasterVerificationGasLimit?: BigNumberish
paymasterPostOpGasLimit?: BigNumberish
paymasterData?: BytesLike
signature: BytesLike
}

export const EIP712_SAFE_OPERATION_TYPE = {
SafeOp: [
Expand Down Expand Up @@ -135,13 +153,13 @@ export const buildSafeUserOpContractCall = async (
)
}

export const buildUserOperationFromSafeUserOperation = ({
export const buildPackedUserOperationFromSafeUserOperation = ({
safeOp,
signature,
}: {
safeOp: SafeUserOperation
signature: string
}): UserOperation => {
}): PackedUserOperation => {
return {
sender: safeOp.safe,
nonce: ethers.toBeHex(safeOp.nonce),
Expand All @@ -154,7 +172,14 @@ export const buildUserOperationFromSafeUserOperation = ({
}
}

export const getRequiredGas = (userOp: UserOperation): string => {
export const buildRpcUserOperationFromSafeUserOperation = (op: {
safeOp: SafeUserOperation
signature: string
}): Promise<UserOperation> => {
return unpackUserOperation(buildPackedUserOperationFromSafeUserOperation(op))
}

export const getRequiredGas = (userOp: PackedUserOperation): string => {
let multiplier = 3n
if (userOp.paymasterAndData === '0x') {
multiplier = 1n
Expand All @@ -164,7 +189,7 @@ export const getRequiredGas = (userOp: UserOperation): string => {
return (BigInt(callGasLimit) + BigInt(verificationGasLimit) * multiplier + BigInt(userOp.preVerificationGas)).toString()
}

export const getRequiredPrefund = (userOp: UserOperation): string => {
export const getRequiredPrefund = (userOp: PackedUserOperation): string => {
const requiredGas = getRequiredGas(userOp)
const { maxFeePerGas } = unpackGasParameters(userOp)
const requiredPrefund = (BigInt(requiredGas) * BigInt(maxFeePerGas)).toString()
Expand Down Expand Up @@ -242,3 +267,59 @@ export const unpackGasParameters = (packed: PackedGasParameters): GasParameters

return { verificationGasLimit, callGasLimit, maxPriorityFeePerGas, maxFeePerGas }
}

/**
* Unpacks a user operation.
*
* @param packedUserOp - The packed user operation.
* @returns The unpacked user operation.
*/
export const unpackUserOperation = async (packedUserOp: PackedUserOperation): Promise<UserOperation> => {
return {
sender: await ethers.resolveAddress(packedUserOp.sender),
nonce: packedUserOp.nonce,
...unpackInitCode(packedUserOp),
callData: packedUserOp.callData,
...unpackGasParameters(packedUserOp),
preVerificationGas: packedUserOp.preVerificationGas,
...unpackPaymasterAndData(packedUserOp),
signature: packedUserOp.signature,
}
}

/**
* Unpacks a user operation's `initCode` field into a factory address and its data.
*
* @param _ - The packed user operation.
* @returns The unpacked `initCode`.
*/
export const unpackInitCode = ({ initCode }: Pick<PackedUserOperation, 'initCode'>): Pick<UserOperation, 'factory' | 'factoryData'> => {
return ethers.dataLength(initCode) > 0
? {
factory: ethers.getAddress(ethers.dataSlice(initCode, 0, 20)),
factoryData: ethers.dataSlice(initCode, 20),
}
: {}
}

/**
* Unpacks a user operation's `paymasterAndData` field into a the paymaster options.
*
* @param _ - The packed user operation.
* @returns The unpacked `paymasterAndData`.
*/
export const unpackPaymasterAndData = ({
paymasterAndData,
}: Pick<PackedUserOperation, 'paymasterAndData'>): Pick<
UserOperation,
'paymaster' | 'paymasterVerificationGasLimit' | 'paymasterPostOpGasLimit' | 'paymasterData'
> => {
return ethers.dataLength(paymasterAndData) > 0
? {
paymaster: ethers.getAddress(ethers.dataSlice(paymasterAndData, 0, 20)),
paymasterVerificationGasLimit: BigInt(ethers.dataSlice(paymasterAndData, 20, 36)),
paymasterPostOpGasLimit: BigInt(ethers.dataSlice(paymasterAndData, 36, 52)),
paymasterData: ethers.dataSlice(paymasterAndData, 52),
}
: {}
}
9 changes: 7 additions & 2 deletions modules/4337/test/e2e/4337NestedSafe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import {
preimageSafeTransactionHash,
signHash,
} from '../../src/utils/execution'
import { buildUserOperationFromSafeUserOperation, buildSafeUserOpTransaction, signSafeOp, SafeUserOperation } from '../../src/utils/userOp'
import {
buildRpcUserOperationFromSafeUserOperation,
buildSafeUserOpTransaction,
signSafeOp,
SafeUserOperation,
} from '../../src/utils/userOp'
import { chainId } from '../utils/encoding'
import { Safe4337 } from '../../src/utils/safe'
import { BUNDLER_MNEMONIC, bundlerRpc, prepareAccounts, waitForUserOp } from '../utils/e2e'
Expand Down Expand Up @@ -403,7 +408,7 @@ describe('E2E - Nested Safes With An Execution Initiated by a Leaf 4337 Safe', (
const executorSigner = executor.owners[0].value as Signer
const safeOp = await buildNestedSafeOp(safeTransaction, tree.root, executionPath, await entryPoint.getAddress())
const signature = buildSignatureBytes([await signSafeOp(executorSigner, await validator.getAddress(), safeOp, await chainId())])
const userOp = buildUserOperationFromSafeUserOperation({
const userOp = await buildRpcUserOperationFromSafeUserOperation({
safeOp,
signature,
})
Expand Down
6 changes: 3 additions & 3 deletions modules/4337/test/e2e/LocalBundler.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect } from 'chai'
import { deployments, ethers, network } from 'hardhat'
import { buildSignatureBytes } from '../../src/utils/execution'
import { buildUserOperationFromSafeUserOperation, buildSafeUserOpTransaction, signSafeOp } from '../../src/utils/userOp'
import { buildRpcUserOperationFromSafeUserOperation, buildSafeUserOpTransaction, signSafeOp } from '../../src/utils/userOp'
import { chainId, timestamp } from '../utils/encoding'
import { Safe4337 } from '../../src/utils/safe'
import { bundlerRpc, prepareAccounts, waitForUserOp } from '../utils/e2e'
Expand Down Expand Up @@ -71,7 +71,7 @@ describe('E2E - Local Bundler', () => {
},
)
const signature = buildSignatureBytes([await signSafeOp(user, await validator.getAddress(), safeOp, await chainId())])
const userOp = buildUserOperationFromSafeUserOperation({
const userOp = await buildRpcUserOperationFromSafeUserOperation({
safeOp,
signature,
})
Expand Down Expand Up @@ -103,7 +103,7 @@ describe('E2E - Local Bundler', () => {
await entryPoint.getAddress(),
)
const signature = buildSignatureBytes([await signSafeOp(user, await validator.getAddress(), safeOp, await chainId())])
const userOp = buildUserOperationFromSafeUserOperation({
const userOp = await buildRpcUserOperationFromSafeUserOperation({
safeOp,
signature,
})
Expand Down
10 changes: 7 additions & 3 deletions modules/4337/test/e2e/SingletonSigners.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { expect } from 'chai'
import { deployments, ethers, network } from 'hardhat'
import { buildSignatureBytes } from '../../src/utils/execution'
import { buildUserOperationFromSafeUserOperation, buildSafeUserOpTransaction } from '../../src/utils/userOp'
import {
buildPackedUserOperationFromSafeUserOperation,
buildRpcUserOperationFromSafeUserOperation,
buildSafeUserOpTransaction,
} from '../../src/utils/userOp'
import { bundlerRpc, encodeMultiSendTransactions, prepareAccounts, waitForUserOp } from '../utils/e2e'

describe('E2E - Singleton Signers', () => {
Expand Down Expand Up @@ -105,7 +109,7 @@ describe('E2E - Singleton Signers', () => {
{ initCode },
)
const opHash = await validator.getOperationHash(
buildUserOperationFromSafeUserOperation({
buildPackedUserOperationFromSafeUserOperation({
safeOp,
signature: '0x',
}),
Expand All @@ -117,7 +121,7 @@ describe('E2E - Singleton Signers', () => {
dynamic: true,
})),
)
const userOp = buildUserOperationFromSafeUserOperation({
const userOp = await buildRpcUserOperationFromSafeUserOperation({
safeOp,
signature,
})
Expand Down
9 changes: 5 additions & 4 deletions modules/4337/test/e2e/UniqueSigner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from 'chai'
import { deployments, ethers, network } from 'hardhat'
import { bundlerRpc, prepareAccounts, waitForUserOp } from '../utils/e2e'
import { chainId } from '../utils/encoding'
import { packGasParameters } from '../../src/utils/userOp'
import { packGasParameters, unpackUserOperation } from '../../src/utils/userOp'

describe('E2E - Unique Signers', () => {
before(function () {
Expand Down Expand Up @@ -92,7 +92,7 @@ describe('E2E - Unique Signers', () => {
const safeSalt = Date.now()
const safe = await proxyFactory.createProxyWithNonce.staticCall(signerLaunchpad.target, launchpadInitializer, safeSalt)

const userOp = {
const packedUserOp = {
sender: safe,
nonce: ethers.toBeHex(await entryPoint.getNonce(safe, 0)),
initCode: ethers.solidityPacked(
Expand Down Expand Up @@ -122,7 +122,7 @@ describe('E2E - Unique Signers', () => {
}

const safeInitOp = {
userOpHash: await entryPoint.getUserOpHash({ ...userOp, signature: '0x' }),
userOpHash: await entryPoint.getUserOpHash({ ...packedUserOp, signature: '0x' }),
validAfter: 0,
validUntil: 0,
entryPoint: entryPoint.target,
Expand All @@ -149,7 +149,8 @@ describe('E2E - Unique Signers', () => {
expect(await ethers.provider.getBalance(safe)).to.equal(ethers.parseEther('1'))
expect(await ethers.provider.getCode(safe)).to.equal('0x')

await bundler.sendUserOperation({ ...userOp, signature }, await entryPoint.getAddress())
const userOp = await unpackUserOperation({ ...packedUserOp, signature })
await bundler.sendUserOperation(userOp, await entryPoint.getAddress())

await waitForUserOp(userOp)
expect(await ethers.provider.getBalance(safe)).to.be.lessThanOrEqual(ethers.parseEther('0.5'))
Expand Down
Loading

0 comments on commit f3129de

Please sign in to comment.