Account Abstraction

Monitoring EIP 4337 Abstracted Accounts

Account abstraction can mean different things to different people, but at its core it is the ability to use a contract as an account. This removes the rigid use of an ECDSA signature as the only way to authorize a transaction. In the past, many smart wallet providers have created their own account abstraction schemes using smart contracts. The goal of EIP 4337 is to standardize this practice and create a more widely used mechanism for abstracting accounts.

Using an Entrypoint contract and a new way of sending transactions, EIP 4337 allows users to send a higher-layer, pseudo-transaction object called a UserOperation. These new transaction objects are passed to the deployed abstracted account contracts for validation, and eventual execution if the transaction is valid.

The functions below will parse the new contracts and transaction types to give you all the information you need to interpret the actions of abstracted accounts.

Note: The examples below are based on the Entrypoint v0.6.0 contract but can easily be adjusted to work for the v0.7.0 contract.

Monitoring All Abstracted Account Transactions

By setting an address trigger on the entrypoint contract with the location restricted to trace_to, we are able to parse every single transaction that is sent to an abstracted account.

{
    "type": "TRIGGER_TYPE_ADDRESS",
    "address": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
    "locations": ["trace_to"]
}
import { ethers } from "https://cdn.skypack.dev/[email protected]";

const HANDLE_OPS_INTERFACE = [
    "function handleOps(tuple(address sender, uint256 nonce, bytes initCode, bytes callData, uint256 callGasLimit, uint256 verificationGasLimit, uint256 preVerificationGas, uint256 maxFeePerGas, uint256 maxPriorityFeePerGas, bytes paymasterAndData, bytes signature)[] ops, address beneficiary)"
];

function removeUnnamedKeys(obj) {
    return Object.fromEntries(
        Object.entries(obj).filter(([key]) => !Number.isInteger(Number(key)))
    );
}

function searchCalls(callObj) {
    var isReverted = false;
    if (callObj["error"] == "execution reverted") {
        isReverted = true;
    }
    if (callObj.calls === undefined || callObj.calls == null) {
        return isReverted;
    }

    for (const call of callObj.calls) {
        var innerRevert = searchCalls(call);
        isReverted = isReverted || innerRevert;
    }

    return isReverted;
}

export function triggerHandler(context, data) {
    try {
        const entrypointIface = new ethers.utils.Interface(HANDLE_OPS_INTERFACE);
        const parsedData = entrypointIface.decodeFunctionData("handleOps", data.input);
        const ops = parsedData.ops.map(removeUnnamedKeys);

        const isReverted = searchCalls(data);

        return { "ops": ops, "reverted": isReverted, "calls": data.calls };
    }
    catch {
        return;
    }
}

Monitoring YOUR Abstracted Account

By setting an address trigger on your abstracted account contract and monitoring incoming calls from the entrypoint contract, you will be notified of all the transactions that are initiated by your wallet contract.

{
    "type": "TRIGGER_TYPE_ADDRESS",
    "address": "YOUR ABSTRACTED ACCOUNT ADDRESS",
    "locations": ["trace_to"]
}
import { ethers } from "https://cdn.skypack.dev/[email protected]";

const ENTRY_POINT_CONTRACT = "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789";
const VALIDATE_USER_FUNC_INTERFACE = [
    "function validateUserOp(tuple(address sender, uint256 nonce, bytes initCode, bytes callData, uint256 callGasLimit, uint256 verificationGasLimit, uint256 preVerificationGas, uint256 maxFeePerGas, uint256 maxPriorityFeePerGas, bytes paymasterAndData, bytes signature) userOp, bytes32 userOpHash, uint256 missingAccountFunds) returns (uint256 validationData)"
]

function removeUnnamedKeys(obj) {
    return Object.fromEntries(
        Object.entries(obj).filter(([key]) => !Number.isInteger(Number(key)))
    );
}

export function triggerHandler(context, data) {
    if (data.from.toLowerCase() != ENTRY_POINT_CONTRACT) return;

    try {
        const iAccountIface = new ethers.utils.Interface(VALIDATE_USER_FUNC_INTERFACE)
        const parsedData = iAccountIface.decodeFunctionData("validateUserOp", data.input);
        const userOp = removeUnnamedKeys(parsedData.userOp);
        return {
            userOp: userOp,
            userOpHash: parsedData.userOpHash,
            missingAccountFunds: parsedData.missingAccountFunds,
        };
    }
    catch {
        return;
    }
}