Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions gator-sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ const sidebar = {
'troubleshooting/invalid-delegate',
'troubleshooting/invalid-delegator',
'troubleshooting/invalid-signature',
'troubleshooting/user-operation-reverted',
],
},
],
Expand Down
111 changes: 111 additions & 0 deletions smart-accounts-kit/troubleshooting/user-operation-reverted.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---
description: How to resolve user operation reverted errors with 0x revert data.
sidebar_label: User operation reverted
toc_max_heading_level: 2
keywords: [user operation, reverted, reason 0x, empty revert, execution, troubleshooting]
---

# User operation reverted

A user operation reverts with reason `0x` when the validation phase passes
but the execution phase fails silently. This is distinct from AA-coded `EntryPoint` contract errors such as `AA23`, `AA25`,
or `AA21`.

When the `EntryPoint` contract calls the smart account's execution function, it uses a low-level
`call` internally. If that inner call reverts with empty data, the bundler surfaces the error as
reason `0x` with no additional information.

The following are common causes.

## Function doesn't exist

The `callData` encodes a call from the smart account to a target contract, but the function
selector doesn't match any function on that contract and no fallback function exists. The EVM
reverts with empty data.

This commonly happens when:
- The function selector has a typo or doesn't match the target's ABI.
- The target address is wrong or points to a different contract.
- The target contract isn't deployed on the current chain.

### Solution

Decode your `callData` and verify the inner call. Check that the target address has deployed code
and that the function selector matches the target's ABI.

```typescript
const code = await publicClient.getCode({
address: targetAddress,
});

if (!code) {
console.log("No contract deployed at this address");
}
```

## Bare revert without a message

The target contract uses `require(false)` or `revert` without a reason string. The revert
returns empty data. Contracts commonly use bare reverts in access control checks, reentrancy locks,
or guard functions.

### Solution

Look at the target contract's source code to identify which `require` or `revert` your call
parameters could trigger.

Use [Tenderly](https://tenderly.co) to simulate the transaction and pinpoint the exact line. See the
[Tenderly debugger documentation](https://docs.tenderly.co/debugger) for details.

## Out of gas in the inner call

Smart accounts use a low-level `call` internally in their `execute` function. When the inner
call runs out of gas, the call returns `false` with empty return data.

This differs from the `AA95` error code, which applies when `handleOps` itself runs out of gas.
In this case, `callGasLimit` might be enough for the smart account's execution overhead but not
enough for the actual target contract call.

### Solution

Increase `callGasLimit`. If you estimate gas manually, try doubling the value. Target
contracts doing complex operations often need more gas than default estimates provide.

## Insufficient balance or allowance

The inner call performs an ERC-20 `transferFrom` but the smart account hasn't approved the
spender, or doesn't hold enough tokens. Some ERC-20 implementations use bare `require` statements
that revert without a reason string.

### Solution

Check that the smart account has sufficient token balance and approvals for the operation.

```typescript
import { erc20Abi } from "viem";

const balance = await publicClient.readContract({
address: tokenAddress,
abi: erc20Abi,
functionName: "balanceOf",
args: [smartAccount.address],
});

const allowance = await publicClient.readContract({
address: tokenAddress,
abi: erc20Abi,
functionName: "allowance",
args: [smartAccount.address, spenderAddress],
});
```

## Manual debugging

If the cause isn't immediately clear, follow these steps:

1. Decode the inner call: Extract the `(to, value, data)` tuple from your `callData` to
understand what the smart account executes.
2. Use Tenderly: Simulate the user operation in [Tenderly](https://tenderly.co) to get a full
execution trace. The trace view shows the exact line where the inner call reverts.
2. Check the basics: Verify the target has deployed code, the smart account has enough
ETH and tokens, and the function selector is correct.
Loading