-
-
Published
Linked with GitHub
# Proposal for account abstraction via alternative mempool
[Account abstraction](https://our.status.im/account-abstraction-eip-2938/) has for a long time been a goal in the Ethereum ecosystem. Account abstraction can allow users to send transactions from smart contract wallets (eg. [social recovery wallets](https://vitalik.ca/general/2021/01/11/recovery.html)) just as easily as they can from regular externally-owned accounts (EOAs). Currently, this is difficult because only an EOA can initiate a transaction and pay for gas. Previous proposals (eg. [EIP 86](https://eips.ethereum.org/EIPS/eip-86), [EIP 2938](https://eips.ethereum.org/EIPS/eip-2938)) attempted to modify the rules to allow smart contracts to pay for gas, but required sophisticated rules to ensure that transactions that pass through the mempool actually are going to pay fees.
This proposal takes a different approach, inspired by (and implementable through) alternate mempools such as [Flashbots](https://github.com/flashbots/pm). We create a separate mempool that contains not _transactions_, but packaged objects representing _calls_ (which we call **user operations**). A miner that is compatible with this mempool would simply include one single transaction which packages and makes all of these calls. Implementing account abstraction inside an alternate mempool allows it to proceed in a low-risk way early on, and eventually it could be more closely enshrined into the protocol (or incorporated through a scheme such as [proposer/block builder separation](https://ethresear.ch/t/proposer-block-builder-separation-friendly-fee-market-designs/9725))
### User operation format
User operations are passed around the network as an SSZ object of the form:
```python
class UserOperation(Container):
target: Address # bytes32
gas: uint64
calldata: bytes
```
`calldata` itself encodes a struct that contains the desired operation that the user wants to make, but the implementation of this is up to the individual smart contract wallet (eg. it could represent an instruction to make one call, or make multiple calls, or perform some key-changing operation in the wallet).
### Wallet format template
Smart contract wallets are expected to have code fitting the following template:
```python
CALLDATA_OFFSET = 160
MAX_CHECK_GAS = 200000
RESERVED_FEE_POS = 0
GASPRICE_POS = 32
POST_CALL_GAS_POS = 64
if (msg.sender == MULTI_SENDER_ADDRESS) {
mstore(CALLDATA_OFFSET - 32, gas())
calldatacopy(0, CALLDATA_OFFSET, calldatasize())
# Self-call to check nonce and signature
# Self-call output expected to be:
# [reserved_fee] [gasprice] [post_call_gas]
require(staticcall(
address(), MAX_CHECK_GAS, 0,
CALLDATA_OFFSET - 32, calldatasize() + 32,
0, CALLDATA_OFFSET
))
# Send reserved fee
call(FEE_RESERVER_ADDRESS, mload(RESERVED_FEE_POS))
# Self-call to do main call(s) [and increment the nonce]
mstore(CALLDATA_OFFSET - 32, -1)
call(
address(), gas() - mload(POST_CALL_GAS_POS), 0,
CALLDATA_OFFSET - 32, calldatasize() + 32,
0, CALLDATA_OFFSET
)
# Ask for refund (non-refunded gas fee gets sent to parent sender)
call(FEE_RESERVER_ADDRESS, GASPRICE_POS, 32, 0, 0)
}
else if (msg.sender != address()) {
# Accept ETH and do nothing
return()
}
else if (calldataload(0) != -1) {
# The first value in calldata of a self-call flags whether we're doing signature-checking or performing an action
...
}
else {
...
}
```
This is a template that nodes in the network could check against; they would only accept a `UserOperation` whose target has code that satisfies this template. Additionally, they would require that the `if (calldataload(0) != -1)` clause in the code must pass a purity check, with the exception that local SLOADs are allowed.
The template ensures the following properties:
* Only a user operation that passes through the wallet can take funds out of the wallet
* Only a user operation that passes through the wallet can cause other user operations to no longer be able to process
* Nodes in the network can halt execution right after the `staticcall` and check memory to see what the reserved fee and gasprice is, and make sure those values are acceptable
These properties are sufficient to make it safe for network nodes to propagate user operations the pass a check of executing past the `staticcall` and stopping there, and for miners to accept such user operations.
### Network node and miner logic
Network nodes would listen for `UserOperation` objects on a dedicated subnet. They would verify a `UserOperation` by running it locally as a call, from their address, until the execution passes the `staticcall` stage. At that point, they would take the `reserved_fee` and compute `implied_gasprice = reserved_fee / (user_operation.gas + 16 * calldatasize() + 2500)`, and check that `gasprice <= implied_gasprice`. They would check:
* That the `staticcall` passes
* That the `implied_gasprice - basefee` is an acceptably high premium
* That `post_call_gas` is sufficient to do the post-call fee refunding
If all checks pass, then as a network node they would propagate the `UserOperation` and as a miner they would consider it for inclusion.
When a new block appears, only `UserOperation`s whose `target` were the target of any `UserOperation` would need to be re-checked (so a theoretical maximum of `gas_limit / 5000` accounts).
When a miner packages a block, they would set the first transaction to be a transaction to the `MULTI_SENDER_ADDRESS`, a specialized contract. They would pass as calldata all the `UserOperations`, which the `MULTI_SENDER_ADDRESS` would then process. At the end, the `MULTI_SENDER_ADDRESS` would claim all fees and pass them along to the miner. Alternatively, bundle proposers in protocols such as Flashbots could produce such a transaction, allowing any Flashbots-compatible miner to include it in their block.