208 lines
7.9 KiB
Markdown
208 lines
7.9 KiB
Markdown
---
|
||
eip: 2770
|
||
title: Meta-Transactions Forwarder Contract
|
||
author: Alex Forshtat (@forshtat), Dror Tirosh (@drortirosh)
|
||
discussions-to: https://ethereum-magicians.org/t/erc-2770-meta-transactions-forwarder-contract/5391
|
||
status: Stagnant
|
||
type: Standards Track
|
||
category: ERC
|
||
created: 2020-07-01
|
||
requires: 712, 2771
|
||
---
|
||
|
||
## Simple Summary
|
||
Standardized contract interface for extensible meta-transaction forwarding.
|
||
|
||
## Abstract
|
||
|
||
This proposal defines an external API of an extensible Forwarder whose responsibility is to validate transaction
|
||
signatures on-chain and expose the signer to the destination contract, that is expected to accommodate all use-cases.
|
||
The ERC-712 structure of the forwarding request can be extended allowing wallets to display readable data even
|
||
for types not known during the Forwarder contract deployment.
|
||
|
||
## Motivation
|
||
|
||
There is a growing interest in making it possible for Ethereum contracts to
|
||
accept calls from externally owned accounts that do not have ETH to pay for
|
||
gas.
|
||
|
||
This can be accomplished with meta-transactions, which are transactions that have been signed as plain data by one
|
||
externally owned account first and then wrapped into an Ethereum transaction by a different account.
|
||
|
||
`msg.sender` is a transaction parameter that can be inspected by a contract to
|
||
determine who signed the transaction. The integrity of this parameter is
|
||
guaranteed by the Ethereum EVM, but for a meta-transaction verifying
|
||
`msg.sender` is insufficient, and signer address must be recovered as well.
|
||
|
||
The Forwarder contract described here allows multiple Gas Relays and Relay Recipient contracts to rely
|
||
on a single instance of the signature verifying code, improving reliability and security
|
||
of any participating meta-transaction framework, as well as avoiding on-chain code duplication.
|
||
|
||
## Specification
|
||
The Forwarder contract operates by accepting a signed typed data together with it's ERC-712 signature,
|
||
performing signature verification of incoming data, appending the signer address to the data field and
|
||
performing a call to the target.
|
||
|
||
### Forwarder data type registration
|
||
Request struct MUST contain the following fields in this exact order:
|
||
```
|
||
struct ForwardRequest {
|
||
address from;
|
||
address to;
|
||
uint256 value;
|
||
uint256 gas;
|
||
uint256 nonce;
|
||
bytes data;
|
||
uint256 validUntil;
|
||
}
|
||
```
|
||
`from` - an externally-owned account making the request \
|
||
`to` - a destination address, normally a smart-contract\
|
||
`value` - an amount of Ether to transfer to the destination\
|
||
`gas` - an amount of gas limit to set for the execution\
|
||
`nonce` - an on-chain tracked nonce of a transaction\
|
||
`data` - the data to be sent to the destination\
|
||
`validUntil` - the highest block number the request can be forwarded in, or 0 if request validity is not time-limited
|
||
|
||
The request struct MAY include any other fields, including nested structs, if necessary.
|
||
In order for the Forwarder to be able to enforce the names of the fields of this struct, only registered types are allowed.
|
||
|
||
Registration MUST be performed in advance by a call to the following method:
|
||
```
|
||
function registerRequestType(string typeName, string typeSuffix)
|
||
```
|
||
`typeName` - a name of a type being registered\
|
||
`typeSuffix` - an ERC-712 compatible description of a type
|
||
|
||
For example, after calling
|
||
```
|
||
registerRequestType("ExtendedRequest", "uint256 x,bytes z,ExtraData extraData)ExtraData(uint256 a,uint256 b,uint256 c)")
|
||
```
|
||
the following ERC-712 type will be registered with forwarder:
|
||
```
|
||
/* primary type */
|
||
struct ExtendedRequest {
|
||
address from;
|
||
address to;
|
||
uint256 value;
|
||
uint256 gas;
|
||
uint256 nonce;
|
||
bytes data;
|
||
uint256 validUntil;
|
||
uint256 x;
|
||
bytes z;
|
||
ExtraData extraData;
|
||
}
|
||
|
||
/* subtype */
|
||
struct ExtraData {
|
||
uint256 a;
|
||
uint256 b;
|
||
uint256 c;
|
||
}
|
||
```
|
||
|
||
### Signature verification
|
||
|
||
The following method performs an ERC-712 signature check on a request:
|
||
```
|
||
function verify(
|
||
ForwardRequest forwardRequest,
|
||
bytes32 domainSeparator,
|
||
bytes32 requestTypeHash,
|
||
bytes suffixData,
|
||
bytes signature
|
||
) view;
|
||
```
|
||
`forwardRequest` - an instance of the `ForwardRequest` struct
|
||
`domainSeparator` - caller-provided domain separator to prevent signature reuse across dapps (refer to ERC-712)
|
||
`requestTypeHash` - hash of the registered relay request type
|
||
`suffixData` - RLP-encoding of the remainder of the request struct
|
||
`signature` - an ERC-712 signature on the concatenation of `forwardRequest` and `suffixData`
|
||
|
||
### Command execution
|
||
|
||
In order for the Forwarder to perform an operation, the following method is to be called:
|
||
```
|
||
function execute(
|
||
ForwardRequest forwardRequest,
|
||
bytes32 domainSeparator,
|
||
bytes32 requestTypeHash,
|
||
bytes suffixData,
|
||
bytes signature
|
||
)
|
||
public
|
||
payable
|
||
returns (
|
||
bool success,
|
||
bytes memory ret
|
||
)
|
||
```
|
||
|
||
Performs the ‘verify’ internally and if it succeeds performs the following call:
|
||
```
|
||
bytes memory data = abi.encodePacked(forwardRequest.data, forwardRequest.from);
|
||
...
|
||
(success, ret) = forwardRequest.to.call{gas: forwardRequest.gas, value: forwardRequest.value}(data);
|
||
```
|
||
Regardless of whether the inner call succeeds or reverts, the nonce is incremented, invalidating the signature and preventing a replay of the request.
|
||
|
||
Note that `gas` parameter behaves according to EVM rules, specifically EIP-150. The forwarder validates internally that
|
||
there is enough gas for the inner call. In case the `forwardRequest` specifies non-zero value, extra `40000 gas` is
|
||
reserved in case inner call reverts or there is a remaining Ether so there is a need to transfer value from the `Forwarder`:
|
||
```solidity
|
||
uint gasForTransfer = 0;
|
||
if ( req.value != 0 ) {
|
||
gasForTransfer = 40000; // buffer in case we need to move Ether after the transaction.
|
||
}
|
||
...
|
||
require(gasleft()*63/64 >= req.gas + gasForTransfer, "FWD: insufficient gas");
|
||
```
|
||
In case there is not enough `value` in the Forwarder the execution of the inner call fails.\
|
||
Be aware that if the inner call ends up transferring Ether to the `Forwarder` in a call that did not originally have `value`, this
|
||
Ether will remain inside `Forwarder` after the transaction is complete.
|
||
|
||
### ERC-712 and 'suffixData' parameter
|
||
`suffixData` field must provide a valid 'tail' of an ERC-712 typed data.
|
||
For instance, in order to sign on the `ExtendedRequest` struct, the data will be a concatenation of the following chunks:
|
||
* `forwardRequest` fields will be RLP-encoded as-is, and variable-length `data` field will be hashed
|
||
* `uint256 x` will be appended entirely as-is
|
||
* `bytes z` will be hashed first
|
||
* `ExtraData extraData` will be hashed as a typed data
|
||
|
||
So a valid `suffixData` is calculated as following:
|
||
```
|
||
function calculateSuffixData(ExtendedRequest request) internal pure returns (bytes) {
|
||
return abi.encode(request.x, keccak256(request.z), hashExtraData(request.extraData));
|
||
}
|
||
|
||
function hashExtraData(ExtraData extraData) internal pure returns (bytes32) {
|
||
return keccak256(abi.encode(
|
||
keccak256("ExtraData(uint256 a,uint256 b,uint256 c)"),
|
||
extraData.a,
|
||
extraData.b,
|
||
extraData.c
|
||
));
|
||
}
|
||
```
|
||
|
||
### Accepting Forwarded calls
|
||
In order to support calls performed via the Forwarder, the Recipient contract must read the signer address from the
|
||
last 20 bytes of `msg.data`, as described in ERC-2771.
|
||
|
||
## Rationale
|
||
Further relying on `msg.sender` to authenticate end users by their externally-owned accounts is taking the Ethereum dapp ecosystem to a dead end.
|
||
|
||
A need for users to own Ether before they can interact with any contract has made a huge portion of use-cases for smart contracts non-viable,
|
||
which in turn limits the mass adoption and enforces this vicious cycle.
|
||
|
||
`validUntil` field uses a block number instead of timestamp in order to allow for better precision and integration
|
||
with other common block-based timers.
|
||
|
||
## Security Considerations
|
||
All contracts introducing support for the Forwarded requests thereby authorize this contract to perform any operation under any account.
|
||
It is critical that this contract has no vulnerabilities or centralization issues.
|
||
|
||
## Copyright
|
||
Copyright and related rights waived via [CC0](../LICENSE.md).
|