Crypto OS
Technical Crypto OSStage 5 · Infrastructure & Security

T27 · Account Abstraction

Can users avoid directly holding a private key and still remain safe?

Capabilities practiced
BuilderProtocol Literacy
Hands on
Deploy a smart account, configure a session key and gas sponsorship, and complete a gas-free action for the user.
AI Lab
Have AI design a social-recovery flow. Describe three ways it could be abused.

A Real-World Problem

You built a lightweight onchain app. The feature itself takes three clicks on first use.

For a new user, the real flow is:

1. Install a wallet extension
2. Copy twelve English words, then select them again in order
3. Buy a little native token somewhere or the first action cannot be sent
4. Click "Start," then confirm a signature popup
5. Click Next and sign again
6. Click Next again and sign a third time

Your funnel shows one hundred visitors, thirty who install a wallet, fifteen who finish the recovery phrase, five who acquire gas, and two who complete their first action.

Nothing is wrong with the product itself. All the drop-off occurs in the act of becoming an onchain account.

Three months later, one of those two users asks for help. They changed phones, lost the wallet, and cannot find the paper note holding the recovery phrase. Can you restore it?

You cannot.

These are two sides of one property: for an externally owned account (EOA), the private key is the entire account. Possess it and you have every permission; lose it and you have none.

There is no “only play this game” permission, no “at most ten dollars per trade” rule, and no recovery procedure after key loss.

Can this all-or-nothing model be split apart?

Thought Experiment

Compare two kinds of account.

First: a safe with one key.

Whoever holds the key controls the safe. The key cannot open only its left half. It cannot be canceled or replaced: the safe recognizes the key, not a person. Lose it and you are locked out forever; have it stolen and your contents are gone.

Here, “who can move the money” is a physical fact.

Second: a company's bank account.

There is no single master key, but a set of rules:

- A cashier may pay up to 5,000
- A finance director may pay up to 50,000
- Above 500,000, two people must sign
- Monthly rent and utilities are recurring payments without individual approvals
- If the chair's seal is lost, publish notice, wait for objections, and issue a new one

These rules do four things the safe cannot: tier by amount, grant permissions by purpose, require several decision-makers, and recover from a lost key.

One difference makes it possible:

In the safe, who can move funds is a physical fact. In the company account, it is a readable, changeable, auditable rule.

Account abstraction puts that rule onchain as code. The account becomes a contract rather than an address derived from a public key. One function answers “Do I authorize this operation?”

So far that sounds ideal. But code creates another question:

Who may change the rule?

A safe has one way to fail: a lost key. A programmable rule adds another: the rule can be changed.

You moved risk from a physical problem into governance. The rest of this chapter handles that new problem.

Your Decision

Return to the funnel. You can improve one thing first.

What Happens

Compare the routes:

First-use hurdleIf a key is lostSingle pointRoutine useNew responsibility for you
EOA with good guidanceHighNo recoveryUser's phraseSign each stepNone
CustodyLowestAsk youYouNearly invisibleHold every user's assets
Smart account and session keyLowDepends on recovery designAccount rulesSmooth within sessionContract security and gas bill
Multiple cooperating keysMediumDepends on recovery designNo single point, but availability riskCoordination each timeCoordination and uptime

Reading the key-loss and single-point columns together gives the first conclusion:

Account abstraction does not remove keys. It divides one key's unlimited power among several keys with different scopes and adds a path to recover after loss.

That is a real improvement: account-level “permissions” gain meaning.

It also demands a second conclusion:

Once permissions can be divided, the boundaries between them become attack surfaces.

An EOA exposes one key. A smart account exposes at least five things: its contract code, session-scope definition, sponsor policy, recovery mechanism, and who can modify all four.

The risk has moved to a new place. That place has one major advantage: it can be audited, tested, and expressed as assertions. You cannot write a test for a lost paper phrase; you can test a session key's scope.

This mirrors T32: structured proposals, deterministic validation, configurable policy, and traceable logs. Account abstraction puts a policy engine in the contract.

Build a Model

The central split: validation versus execution

The design starts by separating two steps:

validate(op)  →  Do the account's rules authorize this operation? (Do not act yet.)
execute(op)   →  Perform it, only after validation passes

For an EOA, a valid signature effectively combines both, with no intermediate rules. In a smart account, authorization becomes code you can write.

The complete path of one operation

The user signs an operation intent rather than sending a transaction. Another party places it onchain.

  1. User signs an operation intent
  2. It enters a dedicated pending pool
  3. Bundler chooses and packages it
  4. EntryPoint dispatches
  5. Account validates
  6. Paymaster decides whether to pay
  7. Account executes
The user signs an intent rather than sending the transaction; its sender and gas payer can differ.

Three roles here do not exist in the EOA flow:

RoleJobIf it fails
BundlerPackage operation intents into an onchain transactionOnly liveness suffers: no bundler means delay, not forgery
EntryPointShared dispatcher and billing entryCommon dependency of every account; requires exceptionally careful audit
PaymasterPay users' gasAn overly broad policy becomes a repeatedly drainable pool

As in the last chapter, a bundler is transport; it affects delivery, not authenticity. “Many bundlers make us secure” confuses those layers.

Smart-account core

Here is the essential part; the rest is surrounding infrastructure:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract SmartAccount {
    address public owner;                 // Primary key
    uint256 public nonce;                 // Replay protection
    address public immutable entryPoint;  // Only accepted entry

    struct SessionKey {
        uint48  validUntil;    // Expiry
        uint128 spendLimit;    // Cumulative limit in smallest units
        uint128 spent;         // Amount used so far
        bool    enabled;
    }

    // Session key → scope
    mapping(address => SessionKey) public sessions;
    // Session key → target contract → function selector → allowed?
    mapping(address => mapping(address => mapping(bytes4 => bool))) public allowed;

    constructor(address ep, address o) {
        entryPoint = ep;
        owner = o;
    }

    modifier onlyEntryPoint() {
        require(msg.sender == entryPoint, "not entry point");
        _;
    }

    /// Validation: decide authorization without external calls
    function validate(
        bytes32 opHash,
        bytes calldata signature,
        address target,
        bytes4  selector,
        uint256 value
    ) external onlyEntryPoint returns (bool) {
        address signer = recover(opHash, signature);

        // Primary key: unrestricted
        if (signer == owner) return true;

        // Session key: check each scope dimension
        SessionKey storage s = sessions[signer];
        if (!s.enabled) return false;
        if (block.timestamp > s.validUntil) return false;
        if (!allowed[signer][target][selector]) return false;
        if (value + s.spent > s.spendLimit) return false;

        // Charge cumulative allowance during validation to prevent repeated uses in a block
        s.spent = uint128(s.spent + value);
        return true;
    }

    /// Execution: called only after EntryPoint accepts validation
    function execute(address target, uint256 value, bytes calldata data)
        external
        onlyEntryPoint
    {
        (bool ok, bytes memory ret) = target.call{value: value}(data);
        if (!ok) {
            assembly { revert(add(ret, 32), mload(ret)) }
        }
    }

    function recover(bytes32, bytes calldata) internal pure returns (address) {
        return address(0); // Replace with signature recovery from your framework
    }
}

Four choices in that code match real failure classes.

First, onlyEntryPoint. Restrict the account's entry, or anyone can call execute and withdraw funds. This directly applies T7's rule to assume any public function will be called by strangers.

Second, deduct the cumulative allowance during validation. If deducted after execution, the same session key can be used repeatedly in one block while spent still shows the old value. It foreshadows reentrancy: state updates after use can be exploited repeatedly.

Third, avoid external calls during validation. Bundlers simulate validation offchain before paying to include an operation. If validation depends on external state, simulation can pass and chain execution fail, leaving the bundler to pay gas. Repeated failures drive bundlers away, so allowed validation behavior is tightly constrained.

Fourth, bubble the original failure from execution. T14 left a receipt-mapping question: a bundle has one transaction status but several inner operations. The account must preserve inner revert data so the interface can show which step failed.

Session keys: scope is everything

A session key is not “a weaker key”; it is a key with a defined scope. That scope decides whether it is useful or dangerous.

A usable scope needs at least six dimensions:

DimensionIf absent
ExpiryA leaked key works forever
Allowed target contractsIt can call any contract, including a token
Allowed function selectorsIt can call every function on an allowed target, including approve
Per-operation limitOne call drains the account
Cumulative limitSplit one large withdrawal into a hundred small ones
Ability to delegate againA session key can issue another session key

The selector row deserves special attention. Restricting only target contracts lets a game session key call an approval-like function on the game contract and grant an attacker-controlled address an allowance. The attacker then withdraws later.

This repeats T32's approval-versus-transfer issue:

Policy must cover every action that changes control of funds, not only transfers.

Paymasters: whoever pays gas needs their own policy

A paymaster pays on others' behalf. An unbounded subsidy is a pool anyone can drain repeatedly.

Three common models:

ModelFinal payerMain risk
Project sponsors allProjectMeaningless high-gas operations exhaust funds
User pays in another tokenUserManipulated conversion price or token transfer hooks
Limited subsidy for first N actionsProject, with limitSybil attack creates thousands of “new” accounts

At minimum, set per-account count and amount limits, a daily global budget, allowed target contracts, and an immediate off switch.

The transfer-hook risk links to T9: a nonstandard token's transfer can hand control away. If payment collection includes that call, the paymaster introduces external execution into a critical path. The next chapter demonstrates what that can do.

Recovery: the most dangerous section of this chapter

A standard social-recovery flow:

1. Owner designates N guardians and a threshold M
2. On loss, M guardians sign a proposal to replace the primary key
3. A timelock begins for T days
4. During T days, the current primary key can cancel unconditionally
5. If not canceled, recovery takes effect after T days

Steps three and four supply the mechanism's security. Without them, M guardians are M keys that can take the account directly.

Even with them, misuse remains possible. Recovery designs often consider only a user losing their key; list other cases:

AbuseWhat happensDoes timelock stop it?
Guardian collusionEnough guardians propose a takeoverOnly partly; user must notice and cancel in time
Guardians not independentFive apparent friends include three addresses controlled by one personNo; threshold is illusory
Social engineeringAttacker impersonates user, asking guardians for helpOnly if user notices and cancels
User unavailableTravel, hospitalization, imprisonment, or death prevents cancellationNo; structural blind spot
Guardians lose keysFewer than M remain available for legitimate recoveryNo; availability failure
Owner abuses recoveryOwner reclaims an account promised to someone elseNo; governance issue

The fourth row matters. A timelock assumes the user can receive notice and cancel within its window. But events that lead to lost keys may also prevent the user from being online.

Three recovery rules matter more than the exact guardian count:

  1. Proposals must emit an onchain event and trigger an offchain notification path. An unseen proposal makes the timelock useless.
  2. The current primary key must cancel unconditionally and without delay. Cancellation should have no extra gate.
  3. Verify guardian independence rather than assume it. Record how and when each guardian was confirmed.

One product decision remains: timelock length is a tradeoff, not merely a technical parameter. Short windows weaken protection against collusion; long ones punish legitimate recovery. Use different windows for different account values, akin to T17's confirmation tiers by business risk.

What It Is Called

Smart AccountSmart account

An account implemented as a contract whose code decides which signatures or operations it accepts.

The central difference from an EOA is cryptographic fact versus a readable, testable, changeable rule for authority.

User OperationUser operation

A signed structured statement of what a user wants: target, data, amount, gas limit, and potential sponsor, rather than a transaction directly.

Someone else includes it onchain, so the transaction sender and gas payer need not be the user. That enables gasless experiences.

BundlerBundler

A party combining user operations into real onchain transactions.

It is transport: its absence delays operations but cannot forge them. It simulates validation before inclusion, which explains why validation is restricted.

EntryPointEntryPoint

The common dispatch and accounting entry for smart accounts. An account trusts calls only from it.

It is the largest shared dependency: if it fails, every account can be affected, so it demands unusually thorough audit.

PaymasterPaymaster

A contract paying users' gas, either from a project's budget or in exchange for another user asset.

The core policy is not “how to pay” but when to refuse. A paymaster without refusal rules is an open cash machine.

Session KeySession key

A temporary scoped key with expiry, targets, selectors, per-call limit, and cumulative limit.

Its scope is everything. Restricting the target but not the selector is a common flaw: an approval call can bypass every spending limit.

Social RecoverySocial recovery

Preselected guardians jointly propose to replace a lost primary key.

Three ingredients are essential: timelock, onchain event plus offchain notice, and unconditional cancellation by the current primary key. Without any one, guardians can become account takers rather than recovery helpers.

Hands-on

Hands onDeploy a smart account with a session key and paymaster, then complete a user action without gasAny Solidity framework and local node or testnetFree on local or testnet faucet funds; do not deploy this exercise code to mainnet or put real assets in it

Use only local networks or testnet. The Lab deliberately creates then repairs vulnerabilities; no step should touch real funds.

Four acceptance checks should each be executable tests:

Check 1: Primary key can execute any authorized operation
Check 2: Session key executes only inside scope; outside scope reverts
Check 3: An account with zero native-token balance still completes an operation
Check 4: Session key cannot bypass amount limits through an approval-like call

Write a minimal account with only a primary key.

Reduce SmartAccount to owner, nonce, validate, and execute, then deploy locally.

Make your first test a negative case: a random address calling execute directly must revert.

Write that test before the feature. Access control is the account's first requirement.

Connect an EntryPoint and exercise the entire flow.

Use your framework to construct and sign a user operation, submit it to a bundler, and inspect it onchain.

Observe:

- Who is the onchain transaction's from address? (Not the user.)
- Which account paid gas?
- How many times did validate run?

The first answer changes how you think about the system: the user's address no longer appears as the transaction sender. Indexers, risk checks, and queries filtering by sender must adapt.

Add a session key limited only by target contract at first.

Allow it to call your app contract and test that calls to another contract revert.

Now perform the Lab's most important adversarial exercise: with that session key, call an approval-like function on your app contract granting another controlled address a large allowance, then withdraw through that address.

It will succeed. Without a selector constraint, the amount limit is flimsy.

Doing this once teaches more than reading the rule repeatedly.

Add selector allowlisting and cumulative limits; make the earlier attack test fail.

This is the central acceptance check: the same test must succeed before the repair (attack works) and fail afterward (attack blocked).

Compilation alone does not prove the fix. The adversarial test changing outcome does.

Test when cumulative spending is deducted.

Within one transaction, use the same session key for several small actions whose combined value exceeds the cumulative limit.

If spent changes only after execution, the test succeeds and so does the attack. Deduct during validation and make it fail.

Remember the shape: state updated after use. The next chapter uses it to drain a contract.

Add a paymaster for gasless use.

Start with an unrestricted sponsor and complete one operation while the user's account holds zero native tokens.

Then add limits immediately: per-account count, daily global budget, target allowlist, and emergency stop.

Write an abuse test issuing many meaningless high-gas operations from one account; the sponsor must reject them after its limit.

Add social recovery and attack it yourself.

Implement guardians, threshold, timelock, and cancellation. After testing normal recovery, add three cases:

- Enough guardians propose; primary key cancels in window: recovery must fail
- Enough guardians propose; nobody cancels: recovery must take effect
- Proposal emits an onchain event with initiator, new primary key, and effective time

The third is not just a feature. It is the user's only way to know someone may be taking the account. Without it, the timelock is an invisible wait.

Finish with a permissions ledger. List every authority:

PartyAllowed actionsLimitLifetimeWho revokes?
Primary keyAny operationNonePermanentRecovery flow only
Session keyAllowlisted functionsPer-call and cumulativeExpiresPrimary key immediately
GuardiansPropose primary-key replacementOnly primary keyOngoingPrimary key in window
PaymasterSponsor account gasDaily budgetOngoingEmergency stop

Trace every row to code. Any blank cell represents a permission you have not designed.

AI Lab

AI LabAsk AI to design social recovery; identify three ways it can be abusedLevel 2 · AI Copilot

Ask in three steps; the third is the real challenge:

Step one:
Design social recovery for a smart account. Specify the full state machine:
states, transition events, and exact actions each role may take in each state.
Describe contract functions, not only abstract behavior.

Step two:
List all trust assumptions as “if ___ cheats or fails, the account will be ___.”
Name concrete roles and numbers.

Step three:
Now act as an attacker. Your goal is to take an account using this recovery flow.
Give at least six attack paths. For each, include:
- The prerequisites you need
- The exact sequence of actions
- Which step of this recovery flow fails to stop you
- Whether the account owner has a chance to notice
Consider social engineering and timing, not only technical methods.

Models often produce a complete-looking first step because social recovery has a standard pattern. Check timelock and whether cancellation has strange extra conditions.

Step two reveals understanding. “If guardians misbehave, the account is taken” is true but nearly empty. Ask how many guardians, within what window, and what the owner can do. Those details turn it into an actionable trust assumption.

Step three is where this Lab pays off, and models are often good at it: enumerating attack paths is one of their strengths. Examples include timing proposals for travel, forging a “security upgrade” notice, compromising notification channels before proposing recovery, and waiting for a guardian to change devices.

But add two they may miss:

First, guardians are not independent. Five apparent friends may include three addresses controlled by one person. The threshold fails while everything looks normal onchain.

Second, recovery can be abused by the owner. They might reclaim an account already promised to another party. That is a governance issue rather than a code exploit—a real risk in jointly managed accounts.

Finally, turn three abuse scenarios into running tests. An attack described in prose has far less evidentiary value than an attack test that executes, the standard since T8.

Verify these points yourself after using AI

  • Does it include a timelock? Without one, guardians can immediately seize the account
  • Can the current primary key cancel a recovery unconditionally and without delay? Question any prerequisites
  • Does a proposal emit an onchain event, and how does offchain notice reach the user? An invisible proposal defeats the lock
  • Does it consider guardians controlled by one person through several addresses, and mitigate that?
  • Does it consider a user unavailable during the window, a structural timelock blind spot?
  • Does it handle guardians losing keys so fewer than the threshold remain available?
  • Is lock duration justified and tiered by account value rather than guessed as one constant?
  • Implement three abuse scenarios as reproducible tests, not just descriptions

Real-World Case

A smart account was deployed but never initialized, and someone else initialized it firstA recurring pattern

Many smart accounts follow a two-step process: deploy a minimal proxy, then have the user call an initializer to set the primary key. If the initializer does not restrict who may call it, anyone can make themselves the owner in the window between deployment and initialization.

When accounts are deployed in batches, that window can last minutes, and it is publicly visible.

This has the same shape as the 2017 library contract incident in T8: an unprotected initialization path. The fix is to initialize in the same transaction as deployment and make the initializer callable only once.

A paymaster was drained by meaningless, high-Gas operationsA common operational incident

To attract users, a project sponsors Gas for everyone without setting any limits.

Someone writes a script that repeatedly submits expensive operations that do nothing useful. Within hours, the sponsorship pool is empty, and legitimate users can no longer use the product.

The attacker did not steal the assets; they made the project burn its own assets on their behalf. The defenses are straightforward: per-account limits on operation count and amount, a global daily budget, and sponsorship only for allowlisted target contracts.

The broader lesson: any design that pays someone else's fees must first specify when it will refuse to pay.

A session key restricted the contract but not the functionThe most common flaw in this kind of implementation

A session key is limited to “calling the game contract,” with a small per-operation limit.

After obtaining the key, the attacker does not transfer funds. Instead, they call an approval function on the game contract, grant a large allowance to an address they control, and withdraw the funds through that address.

The amount limit never fires, because the approval operation itself has a value of zero.

This replays the T32 case at the account layer: if a limit only covers transfers, the attacker changes who controls the funds instead.

A signature omitted the chain ID and was replayed on another chainA typical multi-chain deployment incident

The same smart-account contract is deployed on several chains, and a user's address is the same on each one. Signature verification includes only the nonce and call data; it does not include the chain ID.

A transfer signed by the user on chain A can therefore be replayed unchanged on chain B.

The rule is the same one from the previous chapter, where a cross-chain message's unique ID must include the destination chain: every signature being verified must be bound to the scope where it is valid—the chain, contract address, and nonce. None can be omitted.

Change One Variable

What ifA session key's validity changes from 1 hour to 30 days

The experience improves noticeably: the user does not have to authorize it again for a month.

At the same time, risk grows along three dimensions: the exposure window after a leak is 720 times longer; your application may be upgraded in that period and gain functions the original authorization never anticipated; and the user may long since have forgotten what they authorized.

If a long lifetime is essential, you must compensate with a cumulative spending limit and a narrower function allowlist, and give the user an interface that clearly shows “what I authorized and how much time remains.”

Lifetime and scope are a seesaw. If you extend one, tighten the other.

What ifThe guardians change from five friends to three of your own hardware devices

The risk of collusion and social engineering drops sharply: devices cannot be persuaded.

But availability risk rises sharply: all three devices may be in the same home, where one fire, move, or theft can take them all. The original assumption behind social recovery is that these guardians will not fail at the same time. Devices kept in the same place violate it.

More fundamentally, this turns recovery from “social” back into “custody”: you are back to a safe with one key, except now there are three keys in the same drawer.

What ifThe user pays Gas with a token instead of the native token

The experience improves: the user no longer has to buy the native token before sending a transaction.

But this creates two new dependencies. The first is price: the paymaster needs to know how much Gas the payment token is worth. If that price source can be manipulated, the paymaster will accept payment at a grossly understated price—the attack described in T21 applies here unchanged. The second is the token itself: nonstandard token transfers may have hooks, charge fees, or silently fail under certain conditions. Every quirk listed in T9 becomes a paymaster risk here.

The conclusion: which tokens may pay for Gas is an allowlist, not a switch.

What ifThe account contract itself is upgradeable

The benefits are real: you can fix bugs, add features, and adapt to new standards without making users move their assets.

The cost is a new permission above all the account's other rules: whoever can upgrade the implementation can rewrite every validation rule at once. Session-key limits, recovery timelocks, and paymaster allowlists can all be bypassed.

None of the three proxy rules from T8 can be skipped here. Add one more rule specific to accounts: upgrade authority should belong to the account owner, not the application developer. If it belongs to the developer, the safety of the user's assets is the safety of your private key—you have become a custodian again without saying so.

Takeaway Questions

4
Who is the user?

Who is the user? The answer in this chapter determines the whole technology choice. For a crypto-native user, an EOA with better guidance may be entirely sufficient. For someone hearing about private keys for the first time, copying down a seed phrase is a ceiling they may never get past. Use the new technology for a specific person, not for its own sake.

5
Who is paying?

Who pays? The most visible change brought by account abstraction is that the person initiating a transaction and the person paying Gas can be different. Once separated, this question needs a fresh answer: where the paymaster's money comes from, when it refuses to pay, and whether the product still works after its funds are exhausted. A sponsorship plan that cannot answer these questions becomes an operational incident after launch.

16
What permissions does an agent have?

What permissions does the agent have? A session key is the contract-level answer: lifetime, targets, functions, per-operation amount, cumulative amount, and ability to delegate again. Write all six into the contract, and you have a key that can safely be given to a program. The policy engine in T32 runs offchain; this one runs onchain. Ideally, both are present.

Chapter Check

Key idea

Account abstraction turns "who can sign" into a programmable policy.

Finished this chapter’s lab? Mark it completeView all progress

On this page