Meta-Transactions Forwarder Contract
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
:
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-lengthdata
field will be hasheduint256 x
will be appended entirely as-isbytes z
will be hashed firstExtraData 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.