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 timeYour 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 oneThese 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 hurdle | If a key is lost | Single point | Routine use | New responsibility for you | |
|---|---|---|---|---|---|
| EOA with good guidance | High | No recovery | User's phrase | Sign each step | None |
| Custody | Lowest | Ask you | You | Nearly invisible | Hold every user's assets |
| Smart account and session key | Low | Depends on recovery design | Account rules | Smooth within session | Contract security and gas bill |
| Multiple cooperating keys | Medium | Depends on recovery design | No single point, but availability risk | Coordination each time | Coordination 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 passesFor 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.
- User signs an operation intent
- It enters a dedicated pending pool
- Bundler chooses and packages it
- EntryPoint dispatches
- Account validates
- Paymaster decides whether to pay
- Account executes
Three roles here do not exist in the EOA flow:
| Role | Job | If it fails |
|---|---|---|
| Bundler | Package operation intents into an onchain transaction | Only liveness suffers: no bundler means delay, not forgery |
| EntryPoint | Shared dispatcher and billing entry | Common dependency of every account; requires exceptionally careful audit |
| Paymaster | Pay users' gas | An 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:
| Dimension | If absent |
|---|---|
| Expiry | A leaked key works forever |
| Allowed target contracts | It can call any contract, including a token |
| Allowed function selectors | It can call every function on an allowed target, including approve |
| Per-operation limit | One call drains the account |
| Cumulative limit | Split one large withdrawal into a hundred small ones |
| Ability to delegate again | A 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:
| Model | Final payer | Main risk |
|---|---|---|
| Project sponsors all | Project | Meaningless high-gas operations exhaust funds |
| User pays in another token | User | Manipulated conversion price or token transfer hooks |
| Limited subsidy for first N actions | Project, with limit | Sybil 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 daysSteps 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:
| Abuse | What happens | Does timelock stop it? |
|---|---|---|
| Guardian collusion | Enough guardians propose a takeover | Only partly; user must notice and cancel in time |
| Guardians not independent | Five apparent friends include three addresses controlled by one person | No; threshold is illusory |
| Social engineering | Attacker impersonates user, asking guardians for help | Only if user notices and cancels |
| User unavailable | Travel, hospitalization, imprisonment, or death prevents cancellation | No; structural blind spot |
| Guardians lose keys | Fewer than M remain available for legitimate recovery | No; availability failure |
| Owner abuses recovery | Owner reclaims an account promised to someone else | No; 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:
- Proposals must emit an onchain event and trigger an offchain notification path. An unseen proposal makes the timelock useless.
- The current primary key must cancel unconditionally and without delay. Cancellation should have no extra gate.
- 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
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.
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.
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.
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.
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.
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.
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
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 callWrite 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 timeThe 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:
| Party | Allowed actions | Limit | Lifetime | Who revokes? |
|---|---|---|---|---|
| Primary key | Any operation | None | Permanent | Recovery flow only |
| Session key | Allowlisted functions | Per-call and cumulative | Expires | Primary key immediately |
| Guardians | Propose primary-key replacement | Only primary key | Ongoing | Primary key in window |
| Paymaster | Sponsor account gas | Daily budget | Ongoing | Emergency stop |
Trace every row to code. Any blank cell represents a permission you have not designed.
AI Lab
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
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.
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 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.
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
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.
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.
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.
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
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.
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.
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
It abstracts who is allowed to sign.
For an EOA, that is a cryptographic fact: a signature from the matching private key is valid, with no intermediate state or conditions. For a smart account, it becomes code: that code can check time, amount, target, function, and call count, or require confirmation from several parties.
In one sentence: a cryptographic fact becomes a programmable policy.
The cost is that you are responsible for that policy—if its code is wrong, the error is a vulnerability.
No; it is far from enough.
If only the target contract is restricted, the session key can still call any function on it, including approval functions. A single approval can grant an allowance to an attacker-controlled address. Subsequent transfers do not pass through the account, so none of its amount limits fire—the approval operation itself has a zero amount.
The rule: a policy must cover every action that can change control of funds, not just transfers. Approvals, delegation, administrator changes, and upgrades must all be included.
Because a bundler has to simulate validation offchain before including the operation onchain.
If validation depends on external state, the offchain simulation may pass but the onchain execution may fail. The transaction has already been submitted, and the bundler pays Gas for nothing. Such operations are cheap to construct; repeat them hundreds of times and the bundler may refuse to serve your account.
That is why validation is tightly constrained. At heart, this is an anti-DoS design, not pedantry.
It can stop colluding guardians, provided the account owner sees the proposal during the window and cancels it.
It cannot stop at least three kinds of failure:
- The user is unreachable during the window—and the situations where recovery is most needed often involve being unreachable. This is a structural blind spot of timelocks.
- The guardians are not independent—the user thinks they selected five people, but all five addresses belong to one person. The threshold offers no protection, and nothing looks unusual onchain.
- The notification channel is compromised—the user never receives the proposal alert, so the timelock is just a wait nobody is watching.
A timelock therefore needs two companion safeguards: an onchain event and reliable offchain notification, and the primary key's unconditional, immediate right to cancel. Without all three, guardians can become account takers instead of helpers.
There is no single right answer. Check these points:
- Can every row in the table be traced to code, or do some permissions exist only in documentation?
- Have you filled all six dimensions for every key: lifetime, target, function, per-operation amount, cumulative amount, and ability to delegate again?
- When does the paymaster refuse to pay? Is that rule in the contract or the backend?
- Does a recovery proposal emit an onchain event? How does the notification reach the user?
- Who can upgrade the account contract? Does upgrade authority belong to the user or to you? Is there a timelock?
- Does the policy cover every action that changes control of funds, not just transfers?
- If something goes wrong, can you reconstruct from onchain events “which key did what, at which step”?
Point 5 is the easiest to skip and the one that most determines the nature of the product: if upgrade authority belongs to you, you are the custodian, whatever the product copy says.
Key idea
Account abstraction turns "who can sign" into a programmable policy.