EIP2771 - Secure Protocol for Native Meta Transactions
# Simple Summary
A contract interface for receiving meta transactions through a trusted forwarder.
# Abstract
This ERC defines a minimal contract-level protocol that a compliant Recipient contract needs to support in order to be capable of accepting a meta transaction through a compliant Forwarder contract that it trusts to help it identify the address of the Transaction Signer.
No EVM-level protocol changes are proposed or required.
# 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:
- Authorized by the Transaction Signer. For example, signed by an externally owned account.
- Relayed by an untrusted third party that pays for the gas (the Gas Relay)
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 securing msg.sender
is insufficient.
The problem is that for a contract that is not natively aware of meta transactions, the msg.sender
of the transaction will make it appear to be coming from the Gas Relay and not the Transaction Signer. A secure protocol for a contract to accept meta transactions needs to prevent the Gas Relay from forging, modifying or duplicating requests by the Transaction Signer.
# Specification
The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 (opens new window).
Here is an example flow:
- Transaction Signer - entity that signs & sends to request to Gas Relay
- Gas Relay - receives a signed request off-chain from Transaction Signer and pays gas to turn it into a valid transaction that goes through Trusted Forwarder
- Trusted Forwarder - a contract that is trusted by the
Recipient
to correctly verify the signature and nonce before forwarding the request from Transaction Signer - Recipient - a contract that can securely accept meta-transactions through a Trusted Forwarder by being compliant with this standard.
# Extracting The Transaction Signer address
The Trusted Forwarder is responsible for calling the Recipient contract and MUST append the address of the Transaction Signer (20 bytes of data) to the end of the call data.
For example :
(bool success, bytes memory returnData) = to.call.value(value)(abi.encodePacked(data, from));
The Recipient contract can then extract the Transaction Signer address by performing 3 operations:
- Check that the Forwarder is trusted. How this is implemented is out of the scope of this proposal.
- Extract the Transaction Signer address from the last 20 bytes of the call data and use that as the original
sender
of the transaction (instead ofmsg.sender
) - If the
msg.sender
is not a trusted forwarder (or if the msg.data is shorter than 20 bytes), then return the originalmsg.sender
as it is.
The Recipient MUST check that it trusts the Forwarder to prevent it from extracting address data appended from an untrusted contract. This could result in a forged address.
# Protocol Support Discovery Mechanism
Unless a Recipient contract is being used by a particular frontend that knows that this contract has support for native meta transactions, it would not be possible to offer the user the choice of using meta-transaction to interact with the contract. We thus need a mechanism by which the Recipient can let the world know that it supports meta transactions.
This is especially important for meta transactions to be supported at the Web3 wallet level. Such wallets may not necessarily know anything about the Recipient contract users may wish to interact with.
As a Recipient could trust forwarders with different interfaces and capabilities (e.g., transaction batching, different message signing formats), we need to allow wallets to discover which Forwarder is trusted.
To provide this discovery mechanism a Recipient contract MUST implement this function:
function isTrustedForwarder(address forwarder) external returns(bool);
- That function MUST return true if the forwarder is trusted by the Recipient.
- That function MUST return false if the forwarder is not trusted.
- That function MUST NOT throw a revert.
Internally, the Recipient MUST then accept a request from forwarder
That function can be called on-chain and as such gas restriction needs to be put in place.
A Gas limit of 50k is enough for making the decision either inside the contract, or delegating it to another contract and doing some memory access calculations, like querying a mapping.
# Recipient example
contract RecipientExample {
function purchaseItem(uint256 itemId) external {
address sender = _msgSender();
... perform the purchase for sender
}
address immutable _trustedForwarder;
constructor(address trustedForwarder) internal {
_trustedForwarder = trustedForwarder;
}
function isTrustedForwarder(address forwarder) public returns(bool) {
return forwarder == _trustedForwarder;
}
function _msgSender() internal view returns (address payable signer) {
signer = msg.sender;
if (msg.data.length>=20 && isTrustedForwarder(signer)) {
assembly {
signer := shr(96,calldataload(sub(calldatasize(),20)))
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Rationale
Make it easy for contract developers to add support for meta transactions by standardizing the simplest viable contract interface.
Without support for meta transactions in the recipient contract, an externally owned account can not use meta transactions to interact with the recipient contract.
Without a standard contract interface, there is no standard way for a client to discover whether a recipient supports meta transactions.
Without a standard contract interface, there is no standard way to send a meta transaction to a recipient.
Without the ability to leverage a trusted forwarder every recipient contract has to internally implement the logic required to accept meta transactions securely.
Without a discovery protocol, there is no mechanism for a client to discover whether a recipient supports a specific forwarder.
Making the contract interface agnostic to the internal implementation details of the trusted forwarder, makes it possible for a recipient contract to support multiple forwarders with no change to code.
# Security Considerations
A bad forwarder may allow forgery of the msg.sender
returned from _msgSender()
and allow transactions to appear to be coming from any address.
This means a recipient contract should be very careful which forwarder it trusts and whether this can be modified. The power to change the forwarder trusted by a recipient is equivalent to giving full control over the contract. If this kind of control over the recipient is acceptable, it is recommended that only the owner of the recipient contract be able to modify which forwarder is trusted. Otherwise best to leave it unmodifiable, as in the example above.
# Implementations
An implementation of a base class for a recipient: BaseRelayRecipient.sol (opens new window)
# Copyright
Copyright and related rights waived via CC0 (opens new window).