Gas Free transactions: Meta Transactions explained
All Ethereum transactions have to pay gas fees to be executed, so new users are forced to purchase Ether before they can start using a dapp.
Purchasing Ethers could be a hard task for novice users that often need to go through Know Your Customer and Anti Money-Laundering processes (KYC & AML) of an exchange.
To achieve less user friction one possible approach is Meta Transaction.
There is no trick, the idea is simple: a third-party (the Relayer) sends the user’s transactions and pays for the gas cost.
Simplified Meta Transaction schema
The actors of this scheme are:
- User: signs a meta transaction (that is a message containing information about the transaction he would like to execute).
- Relayer: a web server with a wallet that signs a valid Ethereum transaction (that has the meta transaction as the payload) and sends it to the blockchain.
- Forwarder: an Ethereum contract in charge of verifying the signature of the meta transaction that, not surprisingly, forwards the request to a recipient contract.
- Recipient: the Ethereum contract that the user intended to call without paying gas fee, this contract has to be able to preserve the identity of the user that originally requested the transaction.
On Ethereum, Relayers are organized in a network, in the so-called Open Gas Station Network (OGSN). The mechanism behind the OGSN is the same, but the network of Relayers ensures decentralization, fault tolerance and many other advantages.
But, for now, we want only to replicate the simple mechanism of meta transactions without complicating the situation by introducing in the schema all actors needed for a Relayer Network (Paymasters, Relayer Hub etc.).
The starting point to understand the basic mechanism is given by the OpenZeppelin’s MinimalForwarder implementation, to be used together with an ERC2771 compatible contract as the Recipient contract (this point will be clearer later).
Let’s inspect the MinimalForwarder.sol contract:
contract MinimalForwarder is EIP712
The contract follows the EIP-712 Improvement proposal, a standard for hashing and signing of typed structured data, implementing a domain separator scheme to protect against replay attacks on an eventual fork of the chain.
struct ForwardRequest { address from; address to; uint256 value; uint256 gas; uint256 nonce; bytes data; }
It has a struct type defining the Meta Transaction fields needed.
mapping(address => uint256) private _nonces;
A mapping between addresses and nonces, this because Meta Transactions are not registered on the blockchain, so the contract has to verify on its own the number of meta transactions sent from a given address, to avoid replay attacks.
function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool)
This function verifies that the Meta Transaction (the ForwarderRequest) has a valid signature by the User and its nonce is correct.
function execute(ForwardRequest calldata req, bytes calldata signature) public payable returns (bool, bytes memory) {
After the verification method, this function updates the user’s nonce and forwards the meta transaction to the Recipient contract.
Now, we need an example recipient contract, luckily we have one.
In the following picture, the behavior of this contract.
This contract has in memory a flag object that is initially white and with no owner.
Any user can call the setFlagOwner method of the contract and claim for the flag ownership and paint the flag with the color he prefers.
The recipient contract must be able to deal with both direct and forwarded transactions. The difference between them is the msg.sender value.
In forwarded transactions, the msg.sender is the address of the Forwarder contract. So, in this situation, the recipient contract must retrieve the actual intended msg.sender from the payload of the transaction.
This is targeted by extending the "@openzeppelin/contracts/metatx/ERC2771Context.sol"
contract.
contract Recipient is ERC2771Context
In addition, the contract has to be deployed indicating a trusted Forwarder (the only one enabled to forward transactions to it):
constructor(address trustedForwarder) ERC2771Context(trustedForwarder) {}
and, in the contract code, msg.sender has to be replaced by _msgSender() method.
function setFlagOwner(string memory _color) external { address previousHolder = currentHolder; currentHolder = _msgSender(); color = _color; emit FlagCaptured(previousHolder, currentHolder, color); }
In the code below, the way _msgSender() method is able to retrieve the intended message sender for both direct and forwarded transactions.
function _msgSender() internal view virtual override returns (address sender) { if (isTrustedForwarder(msg.sender)) { assembly { sender := shr(96, calldataload(sub(calldatasize(), 20))) } } else { return msg.sender; } }
Similar approaches that should be investigated are used in exchange protocols like:
- https://wyvernprotocol.com/docs
- https://github.com/etherdelta/smart_contract
- https://protocol.0x.org/en/latest/index.html
- https://github.com/DexyProject/protocol
A running example
A running example is available on Vercel at: https://meta-transaction.vercel.app/ .
A web interface is available to users who want to call the setFlagOwner method without paying gas fee. The only requirement is to have the metamask plugin installed on the browser, for signing messages, but someone has still to pay for gas fees and we agreed that this is the Relayer job. Don’t worry, the gas fees (of course of a testnet) will be payed by a demo Relayer server.
Example web ui
The following picture illustrates more in detail the interactions among the actors behind the scene.
Code of both contracts and demo Relayer server is available at: https://github.com/donpabblo/meta-transaction