Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ERC: UserOperation Builder #361

Merged
merged 32 commits into from
Jun 11, 2024
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
269e4e7
Add ERC: Standardized Smart Account Interfaces
arein Apr 5, 2024
359241a
chore: formart
arein Apr 5, 2024
0e7f429
chore: formart
arein Apr 5, 2024
9155987
chore: format
arein Apr 5, 2024
bc136ab
Update title & description
derekchiang Apr 5, 2024
407b4d2
Use my email instead
derekchiang Apr 5, 2024
4896241
Remove misplaced text
derekchiang Apr 5, 2024
de99b5b
fix frontmatter
bumblefudge Apr 5, 2024
4bfff6d
remove jake upon his offering
arein Apr 5, 2024
73fb3da
chore: change temporary filename for bot
bumblefudge Apr 5, 2024
3ba5e6a
chore: change temporary filename for Wladinator
bumblefudge Apr 5, 2024
a1d83f5
chore: the fleas in Wlad's fur coat
bumblefudge Apr 5, 2024
04f2af5
chore: the fleas in Wlad's fur-lined EIP linking policy
bumblefudge Apr 5, 2024
370a91a
chore: the fleas in Wlad's fur-lined EIP cross-linking policy
bumblefudge Apr 5, 2024
7d13518
chore: the fleamail in Wlad's fur-lined author preamble
bumblefudge Apr 5, 2024
0a06aec
feat: update number
arein Apr 5, 2024
c67916c
feat: number
arein Apr 5, 2024
bd5bf1d
feat: improve counterfactual security
arein Apr 5, 2024
f13808a
Merge branch 'master' into feat/erc-7777
bumblefudge Apr 9, 2024
394ebb4
Merge branch 'master' into feat/erc-7777
bumblefudge Apr 13, 2024
a42f204
Merge branch 'master' into feat/erc-7777
arein May 28, 2024
baf34e1
Merge branch 'master' into feat/erc-7777
arein May 30, 2024
5bfaec1
Merge branch 'master' into feat/erc-7777
arein Jun 5, 2024
a09ee3a
new flow for dummy sig
Jun 6, 2024
cac8d41
Merge pull request #1 from filmakarov/feat/erc-7777
arein Jun 6, 2024
a316f7e
Merge branch 'master' into feat/erc-7777
arein Jun 7, 2024
dec9c24
counterfactual call => reference impl
Jun 10, 2024
51207d8
address comment
Jun 10, 2024
e6c7112
Merge pull request #2 from filmakarov/feat/erc-7777
arein Jun 10, 2024
3bdfae4
Update erc-7679.md
SamWilsn Jun 11, 2024
939fef8
Update erc-7679.md
SamWilsn Jun 11, 2024
281863e
Update erc-7679.md
SamWilsn Jun 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 223 additions & 0 deletions ERCS/erc-7679.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
---
eip: 7679
title: UserOperation Builder
description: Construct UserOperations without being coupled with account-specific logic.
author: Derek Chiang (@derekchiang), Garvit Khatri (@plusminushalf), Fil Makarov (@filmakarov), Kristof Gazso (@kristofgazso), Derek Rein (@arein), Tomas Rocchi (@tomiir), bumblefudge (@bumblefudge)
discussions-to: https://ethereum-magicians.org/t/erc-7679-smart-account-interfaces/19547
status: Draft
type: Standards Track
category: ERC
created: 2024-04-05
requires: 4337
---

## Abstract

Different [ERC-4337](./eip-4337.md) smart account implementations encode their signature, nonce, and calldata differently. This makes it difficult for DApps, wallets, and smart account toolings to integrate with smart accounts without integrating with account-specific SDKs, which introduces vendor lock-in and hurts smart account adoption.

We propose a standard way for smart account implementations to put their account-specific encoding logic on-chain. It can be achieved by implementing methods that accept the raw signature, nonce, or calldata (along with the context) as an input, and output them properly formatted, so the smart account can consume them while validating and executing the User Operation.


## Motivation

At the moment, to build a [ERC-4337](./eip-4337.md) UserOperation (UserOp for short) for a smart account requires detailed knowledge of how the smart account implementation works, since each implementation is free to encode its nonce, calldata, and signature differently.

As a simple example, one account might use an execution function called `executeFoo`, whereas another account might use an execution function called `executeBar`. This will result in the `calldata` being different between the two accounts, even if they are executing the same call.

Therefore, someone who wants to send a UserOp for a given smart account needs to:

* Figure out which smart account implementation the account is using.
* Correctly encode signature/nonce/calldata given the smart account implementation, or use an account-specific SDK that knows how to do that.

In practice, this means that most DApps, wallets, and AA toolings today are tied to a specific smart account implementation, resulting in fragmentation and vendor lock-in.

## Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

### UserOp builder

To conform to this standard, a smart account implementation MUST provide a “UserOp builder” contract that implements the `IUserOperationBuilder` interface, as defined below:


```solidity
struct Execution {
Copy link

@jxom jxom Apr 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why it is termed as "execution" instead of "call"? Feel like we should make terminology symmetrical to EIP-5792. "call" is more aligned to the CALL opcode (and don't forget there is callData right in here!), which is what it ends up transforming into.

address target;
uint256 value;
bytes callData;
}

interface IUserOperationBuilder {
/**
* @dev Returns the ERC-4337 EntryPoint that the account implementation
* supports.
*/
function entryPoint() external view returns (address);

/**
* @dev Returns the nonce to use for the UserOp, given the context.
* @param smartAccount is the address of the UserOp sender.
* @param context is the data required for the UserOp builder to
* properly compute the requested field for the UserOp.
*/
function getNonce(
address smartAccount,
bytes calldata context
) external view returns (uint256);

/**
* @dev Returns the calldata for the UserOp, given the context and
* the executions.
* @param smartAccount is the address of the UserOp sender.
* @param executions are (destination, value, callData) tuples that
* the UserOp wants to execute. It's an array so the UserOp can
* batch executions.
* @param context is the data required for the UserOp builder to
* properly compute the requested field for the UserOp.
*/
function getCallData(
address smartAccount,
Execution[] calldata executions,
bytes calldata context
) external view returns (bytes memory);

/**
* @dev Returns a correctly encoded signature, given a UserOp that
* has been correctly filled out except for the signature field.
* @param smartAccount is the address of the UserOp sender.
* @param userOperation is the UserOp. Every field of the UserOp should
* be valid except for the signature field. The "PackedUserOperation"
* struct is as defined in ERC-4337.
* @param context is the data required for the UserOp builder to
* properly compute the requested field for the UserOp.
*/
function formatSignature(
address smartAccount,
PackedUserOperation calldata userOperation,
bytes calldata context
) external view returns (bytes memory signature);
}
```

### Using the UserOp builder

To build a UserOp using the UserOp builder, the building party SHOULD proceed as follows:

1. Obtain the address of `UserOpBuilder` and a `context` from the account owner. The `context` is an opaque bytes array from the perspective of the building party. The `UserOpBuilder` implementation may need the `context` in order to properly figure out the UserOp fields. See [Rationale](#rationale) for more info.
2. Execute a multicall (batched `eth_call`s) of `getNonce` and `getCallData` with the `context` and executions. The building party will now have obtained the nonce and calldata.
3. Fill out a UserOp with the data obtained previously. Gas values can be set randomly or very low. This userOp will be used to obtain a dummy signature for gas estimations. Sign the hash of userOp. (See [Rationale](#rationale) for what a dummy signature is. See [Security Considerations](#security-considerations) for the details on dummy signature security).
4. Call (via `eth_call`) `formatSignature` with the UserOp and `context` to obtain a UserOp with a properly formatted dummy signature. This userOp can now be used for gas estimation.
5. In the UserOp, change the existing gas values to those obtained from a proper gas estimation. This UserOp must be valid except for the `signature` field. Sign the hash of the UserOp and place the signature in the UserOp.signature field.
6. Call (via `eth_call`) `formatSignature` with the UserOp and `context` to obtain a completely valid UserOp.
1. Note that a UserOp has a lot more fields than `nonce`, `callData`, and `signature`, but how the building party obtains the other fields is outside of the scope of this document, since only these three fields are heavily dependent on the smart account implementation.

At this point, the building party has a completely valid UserOp that they can then submit to a bundler or do whatever it likes with it.

### Using the UserOp builder when the account hasn’t been deployed

To provide the accurate data to the building party, the `UserOpBuilder` will in most cases have to call the account.
If the account has yet to be deployed, which means that the building party is looking to send the very first UserOp for this account, then the building party MAY modify the flow above as follows:

- In addition to the `UserOpBuilder` address and the `context`, the building party also obtains the `factory` and `factoryData` as defined in ERC-4337.
- When calling one of the view functions on the UserOp builder, the building party may use `eth_call` to deploy the `CounterfactualCall` contract, which is going to deploy the account and call `UserOpBuilder` (see below).
- When filling out the UserOp, the building party includes `factory` and `factoryData`.

The `CounterfactualCall` contract SHOULD:
- Deploy the account using `factory` and `factoryData` provided by the building party.
- Revert if the deployment has not succeeded.
- If the account has been deployed succesfully, call `UserOpBuilder` and return the data returned by `UserOpBuilder` to the building party.

See Reference Implementation section for more details on the `CounterfactualCall` contract.

## Rationale

### Context

The `context` is an array of bytes that encodes whatever data the UserOp builder needs in order to correctly determine the nonce, calldata, and signature. Presumably, the `context` is constructed by the account owner, with the help of a wallet software.

Here we outline one possible use of `context`: delegation. Say the account owner wants to delegate a transaction to be executed by the building party. The account owner could encode a signature of the public key of the building party inside the `context`. Let’s call this signature from the account owner the `authorization`.

Then, when the building party fills out the UserOp, it would fill the `signature` field with a signature generated by its own private key. When it calls `getSignature` on the UserOp builder, the UserOp builder would extract the `authorization` from the `context` and concatenates it with the building party’s signature. The smart account would presumably be implemented such that it would recover the building party’s public key from the signature, and check that the public key was in fact signed off by the `authorization`. If the check succeeds, the smart account would execute the UserOp, thus allowing the building party to execute a UserOp on the user’s behalf.

### Dummy signature

The “dummy signature” refers to the signature used in a UserOp sent to a bundler for estimating gas (via `eth_estimateUserOperationGas`). A dummy signature is needed because, at the time the bundler estimates gas, a valid signature does not exist yet, since the valid signature itself depends on the gas values of the UserOp, creating a circular dependency. To break the circular dependency, a dummy signature is used.

However, the dummy signature is not just a fixed value that any smart account can use. The dummy signature must be constructed such that it would cause the UserOp to use about as much gas as a real signature would. Therefore, the dummy signature varies based on the specific validation logic that the smart account uses to validate the UserOp, making it dependent on the smart account implementation.

## Backwards Compatibility

This ERC is intended to be backwards compatible with all ERC-4337 smart accounts as of EntryPoint 0.7.

For smart accounts deployed against EntryPoint 0.6, the `IUserOperationBuilder` interface needs to be modified such that the `PackedUserOperation` struct is replaced with the corresponding struct in EntryPoint 0.6.

## Reference Implementation

### Counterfactual call contract

The counterfactual call contract is inspired by [ERC-6492](./eip-6492.md), which devised a mechanism to execute `isValidSignature` (see [ERC-1271](./eip-1271.md)) against a pre-deployed (counterfactual) contract.

```solidity
contract CounterfactualCall {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need a special handler. Just do
address(factory).call(factoryData)
To create the account, and return its address

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of this handler is to call one of the 7679 functions against it, like getNonceCallData. Am I missing something?

SamWilsn marked this conversation as resolved.
Show resolved Hide resolved

error CounterfactualDeployFailed(bytes error);

constructor(
address smartAccount,
address create2Factory,
bytes memory factoryData,
address userOpBuilder,
bytes memory userOpBuilderCalldata
) {
if (address(smartAccount).code.length == 0) {
(bool success, bytes memory ret) = create2Factory.call(factoryData);
if (!success || address(smartAccount).code.length == 0) revert CounterfactualDeployFailed(ret);
}

assembly {
let success := call(gas(), userOpBuilder, 0, add(userOpBuilderCalldata, 0x20), mload(userOpBuilderCalldata), 0, 0)
let ptr := mload(0x40)
returndatacopy(ptr, 0, returndatasize())
if iszero(success) {
revert(ptr, returndatasize())
}
return(ptr, returndatasize())
}
}

}
```

Here’s an example of calling this contract using the ethers and viem libraries:

```javascript
// ethers
const nonce = await provider.call({
data: ethers.utils.concat([
counterfactualCallBytecode,
(
new ethers.utils.AbiCoder()).encode(['address','address', 'bytes', 'address','bytes'],
[smartAccount, userOpBuilder, getNonceCallData, factory, factoryData]
)
])
})

// viem
const nonce = await client.call({
data: encodeDeployData({
abi: parseAbi(['constructor(address, address, bytes, address, bytes)']),
args: [smartAccount, userOpBuilder, getNonceCalldata, factory, factoryData],
bytecode: counterfactualCallBytecode,
})
})
```

## Security Considerations

### Dummy Signature security

Since the properly formatted dummy signature is going to be publicly disclosed, in theory it can be intercepted and used by the man in the middle. Risks and potential harm of this is very low though as the dummy signature will be effectively unusable after the final UserOp is submitted (as both UserOps use the same nonce). However, to mitigate even this small issue, it is recommended that the UserOp which hash is going to be signed to obtain an un-foirmatted dummy signature (step 3 above) is filled with very low gas values.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
Loading