Meta Transactions and Their Implementation
What Is a Meta Transaction?
In simple terms, a meta transaction is a transaction submitted by a third party on behalf of the user.
The usual flow is:
- The user constructs the transaction parameters and signs them.
- A third party sends the signed payload to a Relay or Forwarder contract.
- The Relay or Forwarder contract verifies the user’s signature.
- The Relay or Forwarder contract calls the target contract.
A normal transaction looks like this:

A meta transaction looks like this:

Note: the Relayer shown in the diagram is the Forwarder contract in this article.
What Can Meta Transactions Be Used For?
Meta transactions are mainly useful in two areas:
User experience
The barrier to entry for decentralized applications is still high. To use a dapp, an ordinary user often has to: a. register on an exchange, deposit funds, complete KYC, and buy ETH b. install MetaMask, store a seed phrase safely, and learn how addresses work c. withdraw assets from the exchange d. understand how decentralized dapps work and how to use them
Any one of these steps is inconvenient. Going through the whole process can easily take hours.
Meta transactions let users interact with a dapp without holding ETH themselves.
Security
Suppose you keep most of your assets in a cold wallet and do not want that wallet to connect directly to the network. With meta transactions, a Relayer or Forwarder contract can submit transactions on behalf of that wallet.
How to Implement Meta Transactions
We can define a data structure like this:
| |
User Signature
The signed payload is built with EIP-712:
| |
The helper _hashTypedDataV4 is implemented according to EIP-712:
| |
Here _EIP712NameHash and _EIP712VersionHash are configured when the contract is deployed.
Then the user signs the resulting digest with their private key:
Verifying the Signature On-Chain
Signature validation depends on two checks:
ecrecoverThe recovered signer fromdigestandsignaturemust match the user’s address.nonceThe nonce inForwardRequestmust match the nonce recorded in the contract.
Verification code:
| |
The msg.sender Problem
When the Forwarder contract eventually calls the target contract, msg.sender inside the target contract is the Forwarder, not the user. If the target contract needs to know the original user, you need an adjustment.
The usual trick is that the Forwarder appends the user’s address to the calldata it forwards. The called function in the target contract does not need to declare that extra parameter explicitly. The compiler has already fixed the parameter layout for the original function, so appending one more value at the end does not break decoding of the existing arguments.
Forwarder execution:
That passes the user address through to the target contract. The remaining question is how the target contract reads it.
First, look at OpenZeppelin’s Context contract:
This is a very fundamental base contract. When I first saw these two functions, they looked pointless. _msgSender() just returns msg.sender, so why not use msg.sender directly? In most cases, that is exactly what people do.
Meta transactions are where _msgSender() becomes useful. We can override _msgSender() so it returns the original user address, as long as the target contract knows which Forwarder is trusted.
In the target contract, the override looks like this:
| |
With that change, the target contract supports forwarded calls. Because OpenZeppelin contracts generally use _msgSender() internally, inherited logic will also work with meta transactions once this override is in place.
There is another possible approach: the target contract could expose a function to temporarily set the user address, the Forwarder could call that setter before the real call, and then reset it afterward. That design is clearly less elegant. It also requires extra storage and therefore more gas.