Simple Summary

Standardized contract interface for extensible meta-transaction forwarding.


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.


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.


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
returns (
   bool success,
   bytes memory ret

Performs the ‘verify’ internally and if it succeeds performs the following call:

bytes memory data = abi.encodePacked(, forwardRequest.from);
(success, ret) ={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-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)"),

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, as described in ERC-2771.


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 and related rights waived via CC0.