Skip to content

Commit

Permalink
detect v06 reverts using code overrides
Browse files Browse the repository at this point in the history
  • Loading branch information
mouseless0x committed Oct 14, 2024
1 parent aac1aae commit 1ba3aef
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 95 deletions.
3 changes: 2 additions & 1 deletion src/cli/config/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ export const rpcArgsSchema = z.object({
"send-transaction-rpc-url": z.string().url().optional(),
"polling-interval": z.number().int().min(0),
"max-block-range": z.number().int().min(0).optional(),
"block-tag-support": z.boolean().optional().default(true)
"block-tag-support": z.boolean().optional().default(true),
"code-override-support": z.boolean().optional().default(false)
})

export const bundleCopmressionArgsSchema = z.object({
Expand Down
6 changes: 6 additions & 0 deletions src/cli/config/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,12 @@ export const rpcOptions: CliCommandOptions<IRpcArgsInput> = {
type: "boolean",
require: false,
default: true
},
"code-override-support": {
description: "Does the RPC support code overrides",
type: "boolean",
require: false,
default: false
}
}

Expand Down
18 changes: 0 additions & 18 deletions src/rpc/estimation/gasEstimationHandler.ts

Large diffs are not rendered by default.

126 changes: 79 additions & 47 deletions src/rpc/estimation/gasEstimationsV06.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { z } from "zod"
import type { SimulateHandleOpResult } from "./types"
import type { AltoConfig } from "../../createConfig"
import { deepHexlify } from "../../utils/userop"

export class GasEstimatorV06 {
private config: AltoConfig
Expand All @@ -25,6 +26,71 @@ export class GasEstimatorV06 {
this.config = config
}

decodeSimulateHandleOpResult(data: Hex): SimulateHandleOpResult {
if (data === "0x") {
throw new RpcError(
"AA23 reverted: UserOperation called non-existant contract, or reverted with 0x",
ValidationErrors.SimulateValidation
)
}

const decodedError = decodeErrorResult({
abi: [...EntryPointV06Abi, ...EntryPointV06SimulationsAbi],
data
})

if (
decodedError &&
decodedError.errorName === "FailedOp" &&
decodedError.args
) {
return {
result: "failed",
data: decodedError.args[1] as string
} as const
}

// custom error thrown by entryPoint if code override is used
if (
decodedError &&
decodedError.errorName === "CallPhaseReverted" &&
decodedError.args
) {
return {
result: "failed",
data: decodedError.args[0]
} as const
}

if (
decodedError &&
decodedError.errorName === "Error" &&
decodedError.args
) {
return {
result: "failed",
data: decodedError.args[0]
} as const
}

if (decodedError.errorName === "ExecutionResult") {
const parsedExecutionResult = executionResultSchema.parse(
decodedError.args
)

return {
result: "execution",
data: {
executionResult: parsedExecutionResult
} as const
}
}

throw new Error(
"Unexpected error whilst decoding simulateHandleOp result"
)
}

async simulateHandleOpV06({
userOperation,
targetAddress,
Expand All @@ -46,6 +112,17 @@ export class GasEstimatorV06 {
const fixedGasLimitForEstimation =
this.config.fixedGasLimitForEstimation

if (this.config.codeOverrideSupport) {
if (stateOverrides === undefined) {
stateOverrides = {}
}

stateOverrides[entryPoint] = {
...deepHexlify(stateOverrides?.[entryPoint] || {}),
code: ENTRYPOINT_V06_SIMULATION_OVERRIDE
}
}

try {
await publicClient.request({
method: "eth_call",
Expand Down Expand Up @@ -116,54 +193,9 @@ export class GasEstimatorV06 {
throw new Error(JSON.stringify(err.cause))
}

const cause = causeParseResult.data
const data = causeParseResult.data.data

if (cause.data === "0x") {
throw new RpcError(
"AA23 reverted: UserOperation called non-existant contract, or reverted with 0x",
ValidationErrors.SimulateValidation
)
}

const decodedError = decodeErrorResult({
abi: [...EntryPointV06Abi, ...EntryPointV06SimulationsAbi],
data: cause.data
})

if (
decodedError &&
decodedError.errorName === "FailedOp" &&
decodedError.args
) {
return {
result: "failed",
data: decodedError.args[1] as string
} as const
}

if (
decodedError &&
decodedError.errorName === "Error" &&
decodedError.args
) {
return {
result: "failed",
data: decodedError.args[0]
} as const
}

if (decodedError.errorName === "ExecutionResult") {
const parsedExecutionResult = executionResultSchema.parse(
decodedError.args
)

return {
result: "execution",
data: {
executionResult: parsedExecutionResult
} as const
}
}
return this.decodeSimulateHandleOpResult(data)
}
throw new Error("Unexpected error")
}
Expand Down
1 change: 0 additions & 1 deletion src/rpc/validation/UnsafeValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,6 @@ export class UnsafeValidator implements InterfaceValidator {
addSenderBalanceOverride,
balanceOverrideEnabled: this.config.balanceOverride,
entryPoint,
replacedEntryPoint: false,
targetAddress: zeroAddress,
targetCallData: "0x",
stateOverrides
Expand Down
28 changes: 28 additions & 0 deletions src/types/contracts/EntryPointSimulationsV6.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
export const EntryPointV06SimulationsAbi = [
{
inputs: [
{
name: "reason",
type: "string"
}
],
name: "Error",
type: "error"
}
] as const

export const EntryPointV07SimulationsAbi = [
{
type: "constructor",
Expand Down
3 changes: 2 additions & 1 deletion src/types/contracts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export * from "./TestOpcodesAccountFactory"
export * from "./TestStorageAccount"
export * from "./SimpleAccountFactory"
export * from "./CodeHashGetter"
export * from "./EntryPointSimulations"
export * from "./EntryPointSimulationsV6"
export * from "./EntryPointSimulationsV7"
export * from "./PimlicoEntryPointSimulations"
1 change: 1 addition & 0 deletions test/e2e/alto-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
"mempool-max-parallel-ops": 10,
"mempool-max-queued-ops": 10,
"enforce-unique-senders-per-bundle": false,
"code-override-support": true,
"enable-instant-bundling-endpoint": true
}
30 changes: 30 additions & 0 deletions test/e2e/tests/eth_estimateUserOperationGas.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { EntryPointVersion } from "viem/account-abstraction"
import { beforeEach, describe, expect, test } from "vitest"
import { beforeEachCleanUp, getSmartAccountClient } from "../src/utils"
import {
getRevertCall,
deployRevertingContract
} from "../src/revertingContract"
import { Address, BaseError } from "viem"

describe.each([
{
Expand All @@ -12,7 +17,10 @@ describe.each([
])(
"$entryPointVersion supports eth_estimateUserOperationGas",
({ entryPointVersion }) => {
let revertingContract: Address

beforeEach(async () => {
revertingContract = await deployRevertingContract()
await beforeEachCleanUp()
})

Expand Down Expand Up @@ -113,5 +121,27 @@ describe.each([
expect(estimation.paymasterPostOpGasLimit).toBe(0n)
expect(estimation.paymasterVerificationGasLimit).toBe(0n)
})

test("Should throw revert reason if simulation reverted during callphase", async () => {
const smartAccountClient = await getSmartAccountClient({
entryPointVersion
})

try {
await smartAccountClient.estimateUserOperationGas({
calls: [
{
to: revertingContract,
data: getRevertCall("foobar"),
value: 0n
}
]
})
} catch (e: any) {
expect(e).toBeInstanceOf(BaseError)
const err = e.walk()
expect(err.reason).toEqual("foobar")
}
})
}
)
88 changes: 74 additions & 14 deletions test/e2e/tests/eth_getUserOperationReceipt.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import type { Address, Hex } from "viem"
import {
parseGwei,
type Address,
type Hex,
getContract,
parseEther,
concat
} from "viem"
import {
type EntryPointVersion,
entryPoint06Address,
entryPoint07Address
entryPoint07Address,
UserOperation,
getUserOperationHash
} from "viem/account-abstraction"
import { beforeAll, beforeEach, describe, expect, test } from "vitest"
import {
Expand All @@ -12,6 +21,8 @@ import {
} from "../src/revertingContract"
import { deployPaymaster } from "../src/testPaymaster"
import { beforeEachCleanUp, getSmartAccountClient } from "../src/utils"
import { deepHexlify } from "permissionless"
import { foundry } from "viem/chains"

describe.each([
{
Expand All @@ -37,28 +48,77 @@ describe.each([
await beforeEachCleanUp()
})

// uses pimlico_sendUserOperationNow to force send a reverting op (because it skips validation)
test("Returns revert bytes when UserOperation reverts", async () => {
const smartAccountClient = await getSmartAccountClient({
entryPointVersion
})

const hash = await smartAccountClient.sendUserOperation({
calls: [
{
to: revertingContract,
data: getRevertCall("foobar"),
value: 0n
}
],
callGasLimit: 500_000n,
verificationGasLimit: 500_000n,
preVerificationGas: 500_000n
const { factory, factoryData } =
await smartAccountClient.account.getFactoryArgs()

let op: UserOperation<typeof entryPointVersion>
if (entryPointVersion === "0.6") {
op = {
callData: await smartAccountClient.account.encodeCalls([
{
to: revertingContract,
data: getRevertCall("foobar"),
value: 0n
}
]),
initCode: concat([factory as Hex, factoryData as Hex]),
paymasterAndData: paymaster,
callGasLimit: 500_000n,
verificationGasLimit: 500_000n,
preVerificationGas: 500_000n,
sender: smartAccountClient.account.address,
nonce: 0n,
maxFeePerGas: parseGwei("10"),
maxPriorityFeePerGas: parseGwei("10")
} as UserOperation<typeof entryPointVersion>
} else {
op = {
sender: smartAccountClient.account.address,
nonce: 0n,
factory,
factoryData,
callData: await smartAccountClient.account.encodeCalls([
{
to: revertingContract,
data: getRevertCall("foobar"),
value: 0n
}
]),
callGasLimit: 500_000n,
verificationGasLimit: 500_000n,
preVerificationGas: 500_000n,
maxFeePerGas: parseGwei("10"),
maxPriorityFeePerGas: parseGwei("10"),
paymaster,
paymasterVerificationGasLimit: 100_000n,
paymasterPostOpGasLimit: 50_000n
} as UserOperation<typeof entryPointVersion>
}

op.signature =
await smartAccountClient.account.signUserOperation(op)

await smartAccountClient.request({
// @ts-ignore
method: "pimlico_sendUserOperationNow",
params: [deepHexlify(op), entryPoint]
})

await new Promise((resolve) => setTimeout(resolve, 1500))

const receipt = await smartAccountClient.getUserOperationReceipt({
hash
hash: getUserOperationHash({
userOperation: op,
chainId: foundry.id,
entryPointAddress: entryPoint,
entryPointVersion
})
})

expect(receipt).not.toBeNull()
Expand Down

0 comments on commit 1ba3aef

Please sign in to comment.