mirror of
https://github.com/status-im/EIPs.git
synced 2025-02-23 12:18:16 +00:00
Hi, I'm a bot! This change was automatically merged because: - It only modifies existing Draft, Review, or Last Call EIP(s) - The PR was approved or written by at least one author of each modified EIP - The build is passing
240 lines
13 KiB
Markdown
240 lines
13 KiB
Markdown
---
|
|
eip: 3074
|
|
title: Native Sponsored Transactions
|
|
author: Sam Wilson (@SamWilsn), Ansgar Dietrichs (@adietrichs), Matt Garnett (@lightclient), Micah Zoltu (@micahzoltu)
|
|
discussions-to: https://ethereum-magicians.org/t/eip-3074-sponsored-transaction-precompile/4880
|
|
status: Draft
|
|
type: Standards Track
|
|
category: Core
|
|
created: 2020-10-15
|
|
---
|
|
|
|
## Simple Summary
|
|
|
|
Creates a new EVM instruction, analogous to `CALL` (`0xF1`), that sets `CALLER` (`0x33`) based on an ECDSA signature.
|
|
|
|
## Abstract
|
|
|
|
This EIP creates an EVM instruction (`CALLFROM`) which forwards a `CALL`, setting `CALLER` according to an ECDSA signature.
|
|
|
|
## Motivation
|
|
|
|
Sponsored transactions—the separation of fee payment from transaction content—have been a long standing feature request. Unlike similar proposals, this EIP specifies a method of implementing sponsored transactions that allows both externally owned accounts (EOAs) and [EIP-2938](./eip-2938.md) contracts to act as sponsors.
|
|
|
|
With the explosion of tokens built on Ethereum, especially stable coins, it has become common for EOAs to hold valuable assets without holding any Ether at all. These assets must be converted to Ether before they can be used to pay gas fees, but without Ether to pay for the conversion, it's impossible to convert them. Sponsored transactions break the circular dependency.
|
|
|
|
While it is possible to emulate sponsored transactions (ex. [Gas Station Network](https://www.opengsn.org/)), these solutions require specific support in callee contracts.
|
|
|
|
## Specification
|
|
|
|
An opcode, at `0xf9`, must function like a `CALL` (`0xF1`) instruction:
|
|
- To the `to` address,
|
|
- Transferring `value` wei from the invoker to the callee,
|
|
- Passing `gas` gas for execution,
|
|
- With calldata in the memory region specified by `argsOffset` and `argsLength`,
|
|
- With a return data region specified by `retOffset` and `retLength`.
|
|
|
|
Additionally, the opcode must:
|
|
|
|
- Set the caller address based on an ECDSA signature.
|
|
|
|
`CALLFROM` must increase the call depth by one, in the same way as `CALL`. `CALLFROM` must not increase the call depth by two (as it would if it first called into the sponsee account and then into the callee.)
|
|
|
|
In a static context (such as the one created by `STATICCALL`), `CALLFROM` with a non-zero `value` must exit the current execution frame immediately (in the same way `CALL` behaves with a non-zero value in a static context.)
|
|
|
|
### Definitions
|
|
|
|
- **`CALLFROM`** - the specific instruction encoded as `0xf9`, introduced by this EIP, which implements the `CALL` analogue.
|
|
- **Transaction-like Package (TLP)** - the signed arguments passed to `CALLFROM`.
|
|
- **Sponsor** - the account which is responsible for paying gas fees and sending the transaction.
|
|
- **Sponsee** - the account which signed the transaction-like package.
|
|
- **Invoker** - the account or contract which contains `CALLFROM`.
|
|
- **Callee** - the target of the call from `CALLFROM`.
|
|
|
|
### Conventions
|
|
|
|
- **`top - N`** - the `N`th most recently pushed value on the EVM stack, where `top - 0` is the most recent.
|
|
- **`||`** - byte concatenation operator.
|
|
|
|
### Constants
|
|
|
|
| Constant | Value | Description |
|
|
| ---------------- | ------ |:----------------------------------------------------------------------------------- |
|
|
| `SPONSORED_TYPE` | `0x03` | [EIP-2718](./eip-2718.md) transaction type reserved for transaction-like packages. |
|
|
|
|
### API
|
|
|
|
#### Inputs
|
|
|
|
`CALLFROM` must require the following stack arguments:
|
|
|
|
| Stack | Value |
|
|
| ---------- | ------------ |
|
|
| `top - 0` | `yParity` |
|
|
| `top - 1` | `r` |
|
|
| `top - 2` | `s` |
|
|
| `top - 3` | `sponsee` |
|
|
| `top - 4` | `extra` |
|
|
| `top - 5` | `gas` |
|
|
| `top - 6` | `callee` |
|
|
| `top - 7` | `value` |
|
|
| `top - 8` | `argsOffset` |
|
|
| `top - 9` | `argsLength` |
|
|
| `top - 10` | `retOffset` |
|
|
| `top - 11` | `retLength` |
|
|
|
|
The signature (`yParity`, `r`, `s`) arguments must be computed from `secp256k1(keccak256(type || abi.encode(invoker, extra)))`.
|
|
|
|
The arguments are defined to be:
|
|
|
|
- `type: uint8` - [EIP-2718](./eip-2718.md) transaction type (always `SPONSORED_TYPE`);
|
|
- `invoker: address` - the address of the invoker contract;
|
|
- `extra: uint256` - extra data, which can be used in the invoker to implement replay protection;
|
|
- `callee: address` - address of the callee;
|
|
- `gas: uint256` - exact gas limit which must be provided with the call into `CALLFROM`;
|
|
- `value: uint256` - exact amount of Ether in wei to be received by the callee;
|
|
- `sponsee: address` - address of the sponsee;
|
|
- `argsOffset: uint256`, `argsLength: uint256` - region of memory used as the calldata for the call into the callee;
|
|
- `retOffset: uint256`, `retLength: uint256` - region of memory filled with the return data from the call into the callee; and
|
|
- `yParity: uint8`, `r: bytes32`, `s: bytes32` - signature for the package.
|
|
|
|
#### Outputs
|
|
|
|
`CALLFROM` pushes the following two values onto the stack:
|
|
|
|
| Stack | Value |
|
|
| ---------- | --------- |
|
|
| `top - 0` | `valid` |
|
|
| `top - 1` | `success` |
|
|
|
|
|
|
##### Output: `valid`
|
|
|
|
`valid` must be zero in the following cases:
|
|
- Invalid signature
|
|
- The address recovered from `yParity`, `r`, and `s` does not match `sponsee`
|
|
- The balance of the invoker is less than `value`
|
|
|
|
`valid` must be a one in all other cases.
|
|
|
|
##### Output: `success`
|
|
|
|
`success` must be zero in the following cases:
|
|
- `success == 0`
|
|
- the code execution failed due to an exceptional halting or revert
|
|
- call depth limit has been reached
|
|
|
|
`success` must be a one in all other cases.
|
|
|
|
##### Output: Return Data
|
|
|
|
The memory region defined by `retOffset` and `retLength` must be filled in the same way as the `CALL` instruction with similar arguments.
|
|
|
|
The return data area accessed with `RETURNDATASIZE` (`0x3d`) and `RETURNDATACOPY` (`0x3e`) must be set in the same way as the `CALL` instruction.
|
|
|
|
### Gas Fees
|
|
|
|
The gas fees for `CALLFROM` are calculated according to the following pseudo-code:
|
|
|
|
```python
|
|
fee = 3200
|
|
|
|
if preconditions_good(...):
|
|
return fee + cost_of_call(...)
|
|
else:
|
|
return fee
|
|
```
|
|
|
|
Where `cost_of_call(...)` is the cost of a `CALL` (`0xF1`) instruction with the same `gas`, `value`, `argsOffset`, `argsLength`, `retOffset`, and `retLength` arguments.
|
|
|
|
## Rationale
|
|
|
|
### Omitting Arguments
|
|
|
|
The `invoker` signature argument is not included in the arguments to the instruction because it can be calculated by the instruction itself.
|
|
|
|
### Two Return Values
|
|
|
|
It is important to differentiate between a failure in `CALLFROM`'s preconditions versus a failure in the callee. Correctly implementing replay protection requires the invoker to change its state (i.e. burn the nonce) even if the callee fails; but doing so if, for example, the signature failed would be nonsensical. Several options exist for encoding these two failure cases: returning two stack elements, reserving a specific revert reason, or choosing different values in a single stack element.
|
|
|
|
First, it's important to note that all three options are a deviation from the semantics of other `CALL` opcodes, but this deviation is unavoidable.
|
|
|
|
Reserving a specific revert reason, for example `CALLFROM failed`, is a large departure from other instructions. An invoker would need to inspect the revert reason to determine whether the callee reverted, or the `CALLFROM` pre-conditions were invalidated, which implies reading and comparing memory values. Further, to remain sound if a callee were to revert with `CALLFROM failed`, `CALLFROM` would need to replace the return data with some other value.
|
|
|
|
Returning a single stack element with different values depending on the situation (ex. `0` on success, `1` when the pre-conditions are violated, and `2` when the callee reverts) introduces the opportunity for a subtle bug: it's trivially easy to misinterpret the return value (`CALL` returns non-zero on success), but it's much harder to ignore a whole new stack value.
|
|
|
|
### Sponsee in Arguments
|
|
|
|
Including `sponsee` in the arguments to `CALLFROM` is a gas optimization for invoker contracts implementing some replay protection based on the sponsee address. Without the `sponsee` argument, invokers would have to do their own `ecrecover` before calling into `CALLFROM` to verify/adjust any state for replay protection.
|
|
|
|
### Reserving an [EIP-2718](./eip-2718.md) Transaction Type
|
|
|
|
While clients should never directly interpret transaction-like packages as true transactions, reserving an [EIP-2718](./eip-2718.md) transaction type for transaction-like packages reduces the likelihood of a transaction-like package being misinterpreted as a true transaction.
|
|
|
|
### Another Sponsored Transaction EIP
|
|
|
|
Other approaches to sponsored transactions, which rely on introducing a new transaction type, are not immediately compatible with account abstraction (AA). These proposals require a _signed_ transaction from the sponsor's account, which is not possible from an AA contract, because it has no private key to sign with.
|
|
|
|
Besides better compatibility with AA, an instruction is a much less intrusive change than a new transaction type. This approach requires no changes in existing wallets, and little change in other tooling.
|
|
|
|
`CALLFROM`'s single purpose is to set `CALLER`. It implements the minimal functionality to enable sender abstraction for sponsored transactions. This single mindedness makes `CALLFROM` significantly more composable with existing Ethereum features.
|
|
|
|
More logic can be implemented around the `CALLFROM` instruction, giving more control to invokers and sponsors without sacrificing security or user experience for sponsees.
|
|
|
|
### What to Sign?
|
|
|
|
Earlier approaches to this problem included mechanisms for replay protection, and also signed over value, gas, and other arguments to `CALLFROM`. Instead, this proposal explicitly delegates these responsibilities to the invoker contract.
|
|
|
|
As originally written, this proposal specified a precompile with storage to track nonces. Since a precompile with storage is unprecedented, a later revision moved replay protection into the invoker contract, necessitating a certain level of user trust in the invoker, while also opening the door to more creative replay protection schemes in the future. Building on this idea of trusted invokers, the other signed fields in the transaction-like package were eliminated until only `invoker` and `extra` remained.
|
|
|
|
The motivation for including `invoker` is to bind a particular transaction-like package to a single invoker. If `invoker` was not part of the TLP, a malicious invoker could reuse the TLP to impersonate the EOA.
|
|
|
|
Finally, `extra` should be used by invoker contracts to implement replay protection and security around calldata, value, and other parameters. For example, an invoker may assume `extra` to be `keccak256(abi.encode(gas, value, nonce))`, guaranteeing that the sponsee intended to set those parameters to those specific values. Without `extra`, invokers would not be able to determine if other values (eg. `gas`, `value`, calldata, etc.) had been tampered with.
|
|
|
|
### On Call Depth
|
|
|
|
The EVM limits the maximum number of nested calls, and naively allowing a sponsor to manipulate the call depth before reaching the invoker would introduce a griefing attack against the sponsee. That said, with the 63/64th gas rule, and the cost of `CALLFROM`, the stack is effectively limited to a much smaller depth than the hard maximum by the `gas` parameter.
|
|
|
|
It is, therefore, sufficient for the invoker to guarantee a minimum amount of gas, because there is no way to reach the hard maximum call depth with any reasonable (i.e. less than billions) amount of gas.
|
|
|
|
## Backwards Compatibility
|
|
|
|
No known issues.
|
|
|
|
## Test Cases
|
|
|
|
TODO
|
|
|
|
## Implementation
|
|
|
|
TODO
|
|
|
|
## Security Considerations
|
|
|
|
### Reentrancy
|
|
|
|
- Checking `msg.sender == tx.origin` no longer prevents reentrancy. _Adding the pre-condition that `sponsor != sponsee` would restore this property._
|
|
|
|
### Signature Verification
|
|
|
|
- Potential impersonation attacks if there is a bug in the signature verification.
|
|
|
|
### Invoker Considerations
|
|
|
|
The following is a non-exhaustive list of checks/pitfalls/conditions that invokers _should_ be wary of:
|
|
|
|
- Replay protection should be implemented by the invoker, and included in `extra`. Without it, a malicious sponsor can replay a TLP, repeating its effects.
|
|
- `value` should be included in `extra`. Without it, a malicious sponsor could cause unexpected effects in the callee.
|
|
- `gas` should be included in `extra`. Without it, a malicious sponsor could cause the callee to run out of gas and fail, griefing the sponsee.
|
|
- The current chain id should be included in `extra` and checked on every transaction. Without it, a malicious sponsor could replay a TLP on a different chain.
|
|
- `callee` and `calldata` should be included in `extra`. Without them, a malicious sponsor may call arbitrary functions in arbitrary contracts.
|
|
|
|
A poorly implemented invoker can _allow a malicious sponsor to take near complete control over a sponsee's EOA_.
|
|
|
|
### Frontrunning
|
|
|
|
- Transaction-like packages can be extracted from the original sponsor's transaction and resent by another sponsor.
|
|
|
|
## Copyright
|
|
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).
|