ERC-2771 Meta Transaction Recipient Protocol (Secure Protocol for Native Meta Transactions)
Final · Accounts & wallets · created 2020-07-01
Defines how a contract can receive requests through a forwarder it trusts and determine the original signer, so that accounts without ETH can use the contract through meta transactions in which a third party pays the gas.
Why it is needed
To call a contract on Ethereum, the sending account must hold ETH to pay for gas. A meta transaction is a scheme in which a third party pays the gas on behalf of a user who has no ETH. The user only signs the request, and a Gas Relay submits that request as an actual transaction. The problem is that the caller the contract sees (msg.sender) is the relay that paid for gas. A contract that does not know about meta transactions cannot tell who actually signed, and cannot stop a relay from forging, altering, or replaying a request. Without a standard interface, wallets and apps also have no way to find out which contracts accept meta transactions or which forwarders they trust. As a result, every contract has to build its own logic for safely receiving requests.
What to implement
function isTrustedForwarder(address forwarder) external view returns(bool);: Returns true if the given address is a forwarder this contract trusts, and false otherwise. It must not end in a revert, and it should not exceed 50,000 gas.
In depth
Four Roles
This standard distinguishes four roles. The Transaction Signer is the party that signs a request and sends it to a gas relay. The Gas Relay receives the signed request off-chain, pays for gas, and turns it into a valid transaction that goes through a trusted forwarder. The Trusted Forwarder is a contract that the recipient contract trusts, and it is responsible for correctly verifying the signature and nonce before forwarding the request. The Recipient is a contract that accepts meta transactions through a trusted forwarder. The gas relay is an untrusted third party, and trust is placed only in the forwarder contract. This standard works only at the contract level, with no protocol changes.
Append the Signer Address to the End of the Call Data
When the trusted forwarder calls the recipient contract, it MUST append the 20-byte address of the transaction signer to the very end of the call data. As an example, the original text shows the form to.call.value(value)(abi.encodePacked(data, from)). It packs the signer address from and concatenates it after the original call data data. Because it only appends to the end without changing the function selector or argument encoding, the recipient contract's existing function signatures remain unchanged. The recipient contract interprets the last 20 bytes as the sender only when the call data length is at least 20 bytes and the caller is a trusted forwarder.
Three Steps for the Recipient to Extract the Signer
The recipient contract determines the original sender in three steps. First, it checks whether the calling forwarder is a trusted forwarder. How trust is managed is outside the scope of this standard. Second, if the forwarder is trusted, it extracts the last 20 bytes of the call data and uses them as the original sender instead of msg.sender. Third, if msg.sender is not a trusted forwarder or msg.data is shorter than 20 bytes, it uses msg.sender as the sender as is. Therefore, it behaves as usual when an ordinary user calls it directly. If the trust check is skipped, any contract could append an address and call it to forge the sender, so this check MUST be performed.
isTrustedForwarder, Which Announces Support
A general wallet that is not a specific frontend cannot know in advance which contracts accept meta transactions. A recipient contract may also trust several forwarders with different capabilities, such as batching transactions or different signature formats, so wallets need a way to find out which forwarders are trusted. Therefore the recipient contract MUST implement the function isTrustedForwarder(address forwarder). It returns true for a trusted forwarder and false otherwise, and it must never end in a revert. Requests from a forwarder for which it returned true must also be accepted internally. Because this function can be called on-chain, it needs a gas limit, and it SHOULD NOT exceed 50,000 gas.
Why msg.sender Alone Is Not Enough
msg.sender is the value a contract uses to check who signed a transaction, and its integrity is guaranteed by the EVM. In a meta transaction, however, the party that actually sends the transaction is the gas relay, so a contract that does not know about meta transactions sees the request as coming from the relay, not the signer. A secure protocol must prevent the relay from forging, modifying, or duplicating the signer's request. This standard places that verification responsibility on the forwarder and requires only the simplest interface from the recipient contract. Because the interface does not depend on the forwarder's internal implementation, a recipient contract can support multiple forwarders without changing its code.
Reading the Reference Implementation
In the original text, RecipientExample stores the forwarder address received in the constructor in the immutable variable _trustedForwarder, and isTrustedForwarder only compares whether the argument equals this address. _msgSender() uses msg.sender by default, and extracts the address with assembly only when msg.data.length is at least 20 and isTrustedForwarder(msg.sender) is true. calldataload(sub(calldatasize(),20)) reads the 32-byte word that includes the last 20 bytes of the call data, and shr(96, ...) shifts right by 96 bits to leave only the 20-byte address. The business function purchaseItem uses the address returned by _msgSender() as the buyer instead of msg.sender. The core of this pattern is using _msgSender() instead of msg.sender wherever the sender is needed.
Things to watch out for
- A Malicious Forwarder Can Forge the Sender: A malicious forwarder can forge the _msgSender() value so that a transaction appears to have been sent from virtually any address. A recipient contract must therefore be very careful when trusting a forwarder. If the forwarder is an upgradeable contract, you must also trust that it will not make a malicious upgrade.
- Changes to the Trusted Forwarder List Should Be Restricted: If the authority to change the trusted forwarder is open, an attacker can register their own address as a trusted forwarder and forge transactions. It is recommended to keep the trusted forwarder list immutable. If that is not possible, only a trusted contract owner should be able to change it.