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).
|