Skip to content

Commit

Permalink
AA-170: validation rules as a separate ERC. (#342)
Browse files Browse the repository at this point in the history
* AA-170: validation rules as a separate ERC.
  • Loading branch information
drortirosh authored Oct 10, 2023
1 parent d2b2762 commit 5b7b971
Show file tree
Hide file tree
Showing 2 changed files with 349 additions and 123 deletions.
131 changes: 8 additions & 123 deletions eip/EIPS/eip-4337.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ status: Draft
type: Standards Track
category: ERC
created: 2021-09-29
requires: eip-aa-rules
---

## Abstract
Expand Down Expand Up @@ -381,40 +382,8 @@ The simulateValidation call returns this range.
A node MAY drop a UserOperation if it expires too soon (e.g. wouldn't make it to the next block)
If the `ValidationResult` includes `sigFail`, the client SHOULD drop the `UserOperation`.

The operations differ in their opcode banning policy.
In order to distinguish between them, there is a call to the NUMBER opcode (`block.number`), used as a delimiter between the 3 functions.
While simulating `userOp` validation, the client should make sure that:

1. May not invokes any **forbidden opcodes**
2. Must not use GAS opcode (unless followed immediately by one of { `CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL` }.)
3. Storage access is limited as follows:
1. self storage (of factory/paymaster, respectively) is allowed, but only if self entity is staked
2. account storage access is allowed (see Storage access by Slots, below),
3. in any case, may not use storage used by another UserOp `sender` in the same bundle (that is, paymaster and factory are not allowed as senders)
4. Limitation on "CALL" opcodes (`CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL`):
1. must not use value (except from account to the entrypoint)
2. must not revert with out-of-gas
3. destination address must have code (EXTCODESIZE>0) or be a standard Ethereum precompile defined at addresses from `0x01` to `0x09`
4. cannot call EntryPoint's methods, except `depositTo` and "fallback"
5. `EXTCODEHASH` of every address accessed (by any opcode) does not change between first and second simulations of the op.
6. `EXTCODEHASH`, `EXTCODESIZE`, `EXTCODECOPY` may not access the EntryPoint contract address or an address with no deployed code.
The exception is accessing the `sender` address from the `factory` contract as it is guaranteed to deploy the `sender`.
7. If `op.initcode.length != 0` , allow only one `CREATE2` opcode call (in the first (deployment) block), otherwise forbid `CREATE2`.

Transient Storage slots defined in [EIP-1153](./eip-1153.md) and accessed using `TLOAD` (`0x5c`) and `TSTORE` (`0x5d`) opcodes
must follow the exact same validation rules as persistent storage if Transient Storage is enabled.

#### Storage associated with an address

We define storage slots as "associated with an address" as all the slots that uniquely related on this address, and cannot be related with any other address.
In solidity, this includes all storage of the contract itself, and any storage of other contracts that use this contract address as a mapping key.

An address `A` is associated with:

1. Slots of contract `A` address itself.
2. Slot `A` on any other address.
3. Slots of type `keccak256(A || X) + n` on any other address. (to cover `mapping(address => value)`, which is usually used for balance in ERC-20 tokens).
`n` is an offset value up to 128, to allow accessing fields in the format `mapping(address => struct)`
In order to prevent DoS attack on bundlers, they must make sure the validation methods above pass the validation rules, which constraint their usage of opcodes and storage.
For the complete procedure see [eip-aa-validation-rules](./eip-aa-rules.md)

#### Alternative Mempools

Expand All @@ -423,10 +392,9 @@ However, there might be use-cases where specific paymasters (and signature aggre
(through manual auditing) and verified that they cannot cause any problem, while still require relaxing of the opcode rules.
A bundler cannot simply "whitelist" request from a specific paymaster: if that paymaster is not accepted by all
bundlers, then its support will be sporadic at best.
Instead, we introduce the term "alternate mempool".
UserOperations that use whitelisted paymasters (or signature aggregators) are put into a separate mempool.
Only bundlers that support this whitelist will use UserOperations from this mempool.
These UserOperations can be bundled together with UserOperations from the main mempool
Instead, we introduce the term "alternate mempool": a modified validation rules, and procedure of propagating them to other bundlers.

The procedure of using alternate mempools is defined in ../eip-aa-rules.md#Alt-mempools-rules

### Bundling

Expand All @@ -440,7 +408,7 @@ During bundling, the client should:

After creating the batch, before including the transaction in a block, the client should:

* Run `debug_traceCall` with maximum possible gas, to enforce the validation opcode and precompile banning and storage access rules,
* Run `debug_traceCall` with maximum possible gas, to enforce the validation rules on opcode and storage access.
as well as to verify the entire `handleOps` batch transaction,
and use the consumed gas for the actual transaction execution.
* If the call reverted, check the `FailedOp` event.
Expand All @@ -462,24 +430,8 @@ it is critical that the exact same opcode and precompile banning rules as well a
for the `handleOps` validation in its entirety as for individual UserOperations.
Otherwise, attackers may be able to use the banned opcodes to detect running on-chain and trigger a `FailedOp` revert.

Banning an offending entity for a given bundler is achieved by increasing its `opsSeen` value by `1000000`
and removing all UserOperations for this entity already present in the mempool.
This change will allow the negative reputation value to deteriorate over time consistent with other banning reasons.

If any of the three conditions is violated, the client should reject the `op`. If both calls succeed (or, if `op.paymaster == ZERO_ADDRESS` and the first call succeeds)without violating the three conditions, the client should accept the op. On a bundler node, the storage keys accessed by both calls must be saved as the `accessList` of the `UserOperation`

When a bundler includes a bundle in a block it must ensure that earlier transactions in the block don't make any UserOperation fail. It should either use access lists to prevent conflicts, or place the bundle as the first transaction in the block.

#### Forbidden opcodes

The forbidden opcodes are to be forbidden when `depth > 2` (i.e. when it is the factory, account, paymaster, or other contracts called by them that are being executed). They are: `GASPRICE`, `GASLIMIT`, `DIFFICULTY`, `TIMESTAMP`, `BASEFEE`, `BLOCKHASH`, `NUMBER`, `SELFBALANCE`, `BALANCE`, `ORIGIN`, `GAS`, `CREATE`, `COINBASE`, `SELFDESTRUCT`. They should only be forbidden during verification, not execution. These opcodes are forbidden because their outputs may differ between simulation and execution, so simulation of calls using these opcodes does not reliably tell what would happen if these calls are later done on-chain.

Exceptions to the forbidden opcodes:

1. A single `CREATE2` is allowed if `op.initcode.length != 0` and must result in the deployment of a previously-undeployed `UserOperation.sender`.
2. `GAS` is allowed if followed immediately by one of { `CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL` }.
(that is, making calls is allowed, using `gasleft()` or `gas` opcode directly is forbidden)

### Reputation scoring and throttling/banning for global entities

#### Reputation Rationale.
Expand All @@ -493,76 +445,9 @@ Note that this stake is never slashed, and can be withdrawn any time (after unst

Unstaked entities are allowed, under the rules below.

When staked, an entity is also allowed to use its own associated storage, in addition to sender's associated storage.
When staked, an entity is less restricted in its memory usage.

The stake value is not enforced on-chain, but specifically by each node while simulating a transaction.
The stake is expected to be above MIN_STAKE_VALUE, and unstake delay above MIN_UNSTAKE_DELAY
The value of MIN_UNSTAKE_DELAY is 84600 (one day)
The value of MIN_STAKE_VALUE is determined per chain, and specified in the "bundler specification test suite"

#### Un-staked entities

Under the following special conditions, unstaked entities still can be used:

* An entity that doesn't use any storage at all, or only the senders's storage (not the entity's storage - that does require a stake)
* An unstaked account can access [storage associated with the sender](#storage-associated-with-an-address) if the UserOp doesn't create a new account (that is initCode is empty), or the UserOp creates a new account using a
staked `factory` contract.
* A paymaster that has a “postOp()” method (that is, validatePaymasterUserOp returns “context”) must be staked

#### Specification.

In the following specification, "entity" is either address that is explicitly referenced by the UserOperation: sender, factory, paymaster and aggregator.
Clients maintain two mappings with a value for staked entities:

* `opsSeen: Map[Address, int]`
* `opsIncluded: Map[Address, int]`

If an entity doesn't use storage at all, or only reference storage associated with the "sender" (see [Storage associated with an address](#storage-associated-with-an-address)), then it is considered "OK", without using the rules below.

When the client learns of a new staked entity, it sets `opsSeen[entity] = 0` and `opsIncluded[entity] = 0` .

The client sets `opsSeen[entity] +=1` each time it adds an op with that `entity` to the `UserOperationPool`, and the client sets `opsIncluded[entity] += 1` each time an op that was in the `UserOperationPool` is included on-chain.

Every hour, the client sets `opsSeen[entity] -= opsSeen[entity] // 24` and `opsIncluded[entity] -= opsIncluded[entity] // 24` for all entities (so both values are 24-hour exponential moving averages).

We define the **status** of an entity as follows:

```python
OK, THROTTLED, BANNED = 0, 1, 2

def status(paymaster: Address,
opsSeen: Map[Address, int],
opsIncluded: Map[Address, int]):
if paymaster not in opsSeen:
return OK
min_expected_included = opsSeen[paymaster] // MIN_INCLUSION_RATE_DENOMINATOR
if min_expected_included <= opsIncluded[paymaster] + THROTTLING_SLACK:
return OK
elif min_expected_included <= opsIncluded[paymaster] + BAN_SLACK:
return THROTTLED
else:
return BANNED
```

Stated in simpler terms, we expect at least `1 / MIN_INCLUSION_RATE_DENOMINATOR` of all ops seen on the network to get included. If an entity falls too far behind this minimum, it gets **throttled** (meaning, the client does not accept ops from that paymaster if there is already an op with that entity, and an op only stays in the pool for 10 blocks), If the entity falls even further behind, it gets **banned**. Throttling and banning naturally decay over time because of the exponential-moving-average rule.

**Non-bundling clients and bundlers should use different settings for the above params**:

| Param | Client setting | Bundler setting |
| - | - | - |
| `MIN_INCLUSION_RATE_DENOMINATOR` | 100 | 10 |
| `THROTTLING_SLACK` | 10 | 10 |
| `BAN_SLACK` | 50 | 50 |

To help make sense of these params, note that a malicious paymaster can at most cause the network (only the p2p network, not the blockchain) to process `BAN_SLACK * MIN_INCLUSION_RATE_DENOMINATOR / 24` non-paying ops per hour.

## Rationale

The main challenge with a purely smart contract wallet based account abstraction system is DoS safety: how can a block builder including an operation make sure that it will actually pay fees, without having to first execute the entire operation? Requiring the block builder to execute the entire operation opens a DoS attack vector, as an attacker could easily send many operations that pretend to pay a fee but then revert at the last moment after a long execution. Similarly, to prevent attackers from cheaply clogging the mempool, nodes in the P2P network need to check if an operation will pay a fee before they are willing to forward it.

In this proposal, we expect accounts to have a `validateUserOp` method that takes as input a `UserOperation`, and verify the signature and pay the fee. This method is required to be almost-pure: it is only allowed to access the storage of the account itself, cannot use environment opcodes (eg. `TIMESTAMP`), and can only edit the storage of the account, and can also send out ETH (needed to pay the entry point). The method is gas-limited by the `verificationGasLimit` of the `UserOperation`; nodes can choose to reject operations whose `verificationGasLimit` is too high. These restrictions allow block builders and network nodes to simulate the verification step locally, and be confident that the result will match the result when the operation actually gets included into a block.

The entry point-based approach allows for a clean separation between verification and execution, and keeps accounts' logic simple. The alternative would be to require accounts to follow a template where they first self-call to verify and then self-call to execute (so that the execution is sandboxed and cannot cause the fee payment to revert); template-based approaches were rejected due to being harder to implement, as existing code compilation and verification tooling is not designed around template verification.

### Paymasters

Expand Down
Loading

0 comments on commit 5b7b971

Please sign in to comment.