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:
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
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:
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:
uintgasForTransfer=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.