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

Feat/detect v06 reverts #327

Merged
merged 2 commits into from
Oct 14, 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
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.

127 changes: 80 additions & 47 deletions src/rpc/estimation/gasEstimationsV06.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
ENTRYPOINT_V06_SIMULATION_OVERRIDE,
EntryPointV06Abi,
EntryPointV06SimulationsAbi,
RpcError,
Expand All @@ -17,6 +18,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 +27,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 +113,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 +194,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
Loading