π οΈ Debugging Guide for UserOperations in Account Abstraction β
When sending a UserOperation
to the bundler, it can sometimes be unclear if it was included in a block or reverted. Here's how you can debug step-by-step.
1οΈβ£ Understanding Bundler's Receipt via eth_getUserOperationByHash
β
The first step is to poll the bundler to check if the UserOperation
has been included using:
const response = await fetch(BUNDLER_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "eth_getUserOperationByHash",
params: [userOpHash],
}),
});
The response structure looks like this if the UserOperation
has been included:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"userOperationHash": "0x...",
"sender": "0x...",
"nonce": "0x0",
"paymaster": "0x...",
"transactionHash": "0x...",
"success": true,
"reason": null,
"gasUsed": "0x...",
"actualGasCost": "0x..."
}
}
β transactionHash present β Operation was included
β transactionHash null β Still pending or dropped
If success
is false
, you may also see a reason
field with the revert reason (if the bundler captured it).
2οΈβ£ Checking Block Explorer (Blockscout) β
If the transactionHash
is present, go to the testnet block explorer like:
- https://explorer-testnet.soneium.org/tx/<transactionHash>
- https://soneium-minato.blockscout.com/tx/<transactionHash>
You'll see the following:
- Status: Success or Failed
- Gas used / Actual cost
- Logs & Events (Useful for checking emitted logs or debugging)
- Function call data (decoded input data)
Blockscout also now supports ERC-4337-specific views for UserOperations
. Look for:
- UserOp Hash
- Sender (smart account)
- Paymaster used
- EntryPoint contract
- Bundler address
If status is failed, check:
- Whether the smart account was deployed
initCode
was correct- Paymaster had enough balance or permission
revertReason
from bundler response (if any)
3οΈβ£ Using Tenderly for Deep Debugging β
To debug deeper (like step-by-step trace of contract calls), you can use Tenderly:
β How to Use Tenderly: β
- Find your transaction hash via the bundler receipt or blockscout.
- Open:
https://dashboard.tenderly.co/public/<your-project>/tx/<transactionHash>
Replace <your-project>
with your public project name if you have one. If you're using a private workspace, login to Tenderly and go to the transaction dashboard.
- View Execution Trace:
- See all internal contract calls
- Hover over steps to view decoded inputs and return values
- Identify the exact contract and function where failure occurred
- Check Gas Report & State Changes:
- Analyze which call consumed how much gas
- Inspect changes to smart contract storage
π You can even fork the transaction and simulate fixes directly in Tenderly.
βοΈ Simulating UserOperation
s in Tenderly β
Simulating a UserOperation
in Tenderly can help catch failures before sending it to the bundler. It provides detailed traces, reverts, and state diffs which make debugging super intuitive.
π§ͺ Why Use Simulation? β
Before sending a UserOperation
, it's good practice to simulate it using either:
EntryPoint.simulateValidation()
β to check if the bundler would accept it.EntryPoint.simulateHandleOp()
β to simulate full execution includingexecute()
call.- Or simulate the full
eth_sendUserOperation
payload directly in Tenderly!
π Steps to Simulate a UserOperation in Tenderly β
1οΈβ£ Create a Fork or Project in Tenderly β
- Go to Tenderly Dashboard
- Create a new project or use an existing one.
- Create a fork of your target network (e.g., Base Sepolia, Soneium Testnet)
2οΈβ£ Prepare Your Payload β
Prepare the full UserOperation
as JSON that you'd send to the bundler:
{
"sender": "0xYourSmartAccount",
"nonce": "0x0",
"initCode": "0x...",
"callData": "0x...",
"callGasLimit": "0x...",
"verificationGasLimit": "0x...",
"preVerificationGas": "0x...",
"maxFeePerGas": "0x...",
"maxPriorityFeePerGas": "0x...",
"paymaster": "0xYourPaymaster",
"paymasterData": "0x...",
"signature": "0x..."
}
3οΈβ£ Open Tenderly Simulator β
- Go to your Fork in Tenderly
- Click "Simulate Transaction" β then choose Custom Call
Set the parameters:
- From:
EntryPoint
contract address - To:
EntryPoint
contract address - Input: ABI-encoded call to
handleOps([userOp], beneficiary)
You can encode this using ethers.js:
entryPointInterface.encodeFunctionData("handleOps", [[userOp], beneficiary])
Set:
- Gas limit: sufficiently high (e.g.,
10,000,000
) - Value: 0
- State override: You can override the
sender
smart account storage if itβs not deployed yet
4οΈβ£ Run the Simulation β
Click Simulate. Youβll see:
- π Every internal call
- π₯ Revert reasons
- π Gas breakdown
- π§ State diffs
- π§΅ Stack traces with contract-level debug
π§ββοΈ Pro Tips β
- If your smart account isn't deployed yet, simulate with
initCode
and override its nonce or code hash in the state override tab. - For Paymaster failures, ensure you override the paymaster's storage/balance if needed.
- Use the Tenderly CLI to script simulations in CI pipelines (e.g., before sending real UserOps).
β Example Debug Flow with Tenderly β
- Generate the UserOp off-chain using your SDK
- Encode the
handleOps([userOp], beneficiary)
call - Simulate in Tenderly using your fork
- If simulation fails:
- Check
signature
- Check
initCode
is correct - Confirm Paymaster is funded/whitelisted
- Inspect revert reason inside your smart account logic
- Check
β Final Tips β
If the transaction is not included after retries, check if:
- Your bundler is alive and reachable.
- Your
UserOperation
passed all simulation checks (e.g.,simulateValidation
). - Gas limits are correctly estimated and funded.
Use console logs in your scripts to track:
console.log("UserOpHash:", userOpHash);
console.log("TransactionHash:", result.result?.transactionHash);
console.log("Revert reason:", result.result?.reason);
- Reach out to us on one of the support channels if you are still not able to pass a successful transaction.