0
# Meta-Transactions
1
2
OpenZeppelin Contracts provides ERC-2771 meta-transaction support enabling gasless transactions and improved user experience in decentralized applications by allowing third parties to pay gas fees on behalf of users.
3
4
## Core Imports
5
6
Import meta-transaction contracts using Solidity import statements:
7
8
```solidity
9
import "@openzeppelin/contracts/metatx/ERC2771Context.sol";
10
import "@openzeppelin/contracts/metatx/MinimalForwarder.sol";
11
```
12
13
## Capabilities
14
15
### ERC2771 Context
16
17
Context variant that supports meta-transactions by extracting the actual sender from the call data when transactions are forwarded through trusted forwarder contracts.
18
19
```solidity { .api }
20
abstract contract ERC2771Context is Context {
21
constructor(address trustedForwarder);
22
23
function isTrustedForwarder(address forwarder) public view virtual returns (bool);
24
function _msgSender() internal view virtual override returns (address sender);
25
function _msgData() internal view virtual override returns (bytes calldata);
26
}
27
```
28
29
#### Usage Example
30
31
```solidity
32
pragma solidity ^0.8.0;
33
34
import "@openzeppelin/contracts/metatx/ERC2771Context.sol";
35
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
36
37
contract MetaToken is ERC20, ERC2771Context {
38
constructor(
39
string memory name,
40
string memory symbol,
41
address trustedForwarder
42
) ERC20(name, symbol) ERC2771Context(trustedForwarder) {
43
_mint(msg.sender, 1000000 * 10**18);
44
}
45
46
function transfer(address to, uint256 amount) public virtual override returns (bool) {
47
address owner = _msgSender(); // Gets actual sender, even in meta-tx
48
_transfer(owner, to, amount);
49
return true;
50
}
51
52
function _msgSender() internal view virtual override(Context, ERC2771Context) returns (address) {
53
return ERC2771Context._msgSender();
54
}
55
56
function _msgData() internal view virtual override(Context, ERC2771Context) returns (bytes calldata) {
57
return ERC2771Context._msgData();
58
}
59
}
60
```
61
62
### Minimal Forwarder
63
64
Simple implementation of a meta-transaction forwarder that verifies signatures and executes calls on behalf of users.
65
66
```solidity { .api }
67
contract MinimalForwarder is EIP712 {
68
struct ForwardRequest {
69
address from;
70
address to;
71
uint256 value;
72
uint256 gas;
73
uint256 nonce;
74
bytes data;
75
}
76
77
function getNonce(address from) public view returns (uint256);
78
function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool);
79
function execute(ForwardRequest calldata req, bytes calldata signature) public payable returns (bool, bytes memory);
80
}
81
```
82
83
#### Events
84
85
```solidity { .api }
86
event ExecutedForwardRequest(address indexed from, uint256 nonce, bool success);
87
```
88
89
#### Usage Example
90
91
```solidity
92
pragma solidity ^0.8.0;
93
94
import "@openzeppelin/contracts/metatx/MinimalForwarder.sol";
95
96
// Deploy the forwarder
97
contract MetaTxSetup {
98
MinimalForwarder public forwarder;
99
MetaToken public token;
100
101
constructor() {
102
forwarder = new MinimalForwarder();
103
token = new MetaToken("MetaToken", "META", address(forwarder));
104
}
105
106
function executeMetaTransaction(
107
MinimalForwarder.ForwardRequest calldata req,
108
bytes calldata signature
109
) external {
110
require(forwarder.verify(req, signature), "Invalid signature");
111
forwarder.execute(req, signature);
112
}
113
}
114
```
115
116
## Client-Side Meta-Transaction Implementation
117
118
### JavaScript/TypeScript Example
119
120
```javascript
121
// Client-side code for creating meta-transactions
122
const ethers = require('ethers');
123
124
class MetaTransactionClient {
125
constructor(forwarderAddress, forwarderAbi, signer) {
126
this.forwarder = new ethers.Contract(forwarderAddress, forwarderAbi, signer);
127
this.signer = signer;
128
}
129
130
async createMetaTransaction(to, data, value = 0, gas = 100000) {
131
const from = await this.signer.getAddress();
132
const nonce = await this.forwarder.getNonce(from);
133
134
const request = {
135
from,
136
to,
137
value,
138
gas,
139
nonce,
140
data
141
};
142
143
// Create EIP-712 typed data
144
const domain = {
145
name: 'MinimalForwarder',
146
version: '0.0.1',
147
chainId: await this.signer.getChainId(),
148
verifyingContract: this.forwarder.address
149
};
150
151
const types = {
152
ForwardRequest: [
153
{ name: 'from', type: 'address' },
154
{ name: 'to', type: 'address' },
155
{ name: 'value', type: 'uint256' },
156
{ name: 'gas', type: 'uint256' },
157
{ name: 'nonce', type: 'uint256' },
158
{ name: 'data', type: 'bytes' }
159
]
160
};
161
162
// Sign the meta-transaction
163
const signature = await this.signer._signTypedData(domain, types, request);
164
165
return { request, signature };
166
}
167
168
async executeMetaTransaction(request, signature) {
169
return await this.forwarder.execute(request, signature);
170
}
171
}
172
173
// Usage
174
async function sendMetaTransaction() {
175
const client = new MetaTransactionClient(forwarderAddress, forwarderAbi, userSigner);
176
177
// Create a token transfer call
178
const tokenInterface = new ethers.utils.Interface(['function transfer(address,uint256)']);
179
const data = tokenInterface.encodeFunctionData('transfer', [recipient, amount]);
180
181
const { request, signature } = await client.createMetaTransaction(
182
tokenAddress,
183
data
184
);
185
186
// Send to relayer or execute directly
187
await client.executeMetaTransaction(request, signature);
188
}
189
```
190
191
## Advanced Meta-Transaction Patterns
192
193
### Batch Meta-Transactions
194
195
```solidity
196
pragma solidity ^0.8.0;
197
198
import "@openzeppelin/contracts/metatx/MinimalForwarder.sol";
199
200
contract BatchForwarder is MinimalForwarder {
201
struct BatchRequest {
202
ForwardRequest[] requests;
203
uint256 deadline;
204
}
205
206
function executeBatch(
207
BatchRequest calldata batchReq,
208
bytes[] calldata signatures
209
) external returns (bool[] memory successes, bytes[] memory results) {
210
require(block.timestamp <= batchReq.deadline, "Batch expired");
211
require(batchReq.requests.length == signatures.length, "Length mismatch");
212
213
successes = new bool[](batchReq.requests.length);
214
results = new bytes[](batchReq.requests.length);
215
216
for (uint256 i = 0; i < batchReq.requests.length; i++) {
217
require(verify(batchReq.requests[i], signatures[i]), "Invalid signature");
218
(successes[i], results[i]) = execute(batchReq.requests[i], signatures[i]);
219
}
220
}
221
}
222
```
223
224
### Conditional Meta-Transactions
225
226
```solidity
227
pragma solidity ^0.8.0;
228
229
import "@openzeppelin/contracts/metatx/ERC2771Context.sol";
230
231
contract ConditionalMetaTx is ERC2771Context {
232
mapping(address => uint256) public balances;
233
mapping(bytes32 => bool) public executedConditions;
234
235
struct ConditionalTransfer {
236
address from;
237
address to;
238
uint256 amount;
239
uint256 minBalance;
240
uint256 deadline;
241
bytes32 conditionHash;
242
}
243
244
constructor(address trustedForwarder) ERC2771Context(trustedForwarder) {}
245
246
function executeConditionalTransfer(
247
ConditionalTransfer calldata transfer
248
) external {
249
require(block.timestamp <= transfer.deadline, "Transfer expired");
250
require(!executedConditions[transfer.conditionHash], "Already executed");
251
require(balances[transfer.from] >= transfer.minBalance, "Condition not met");
252
require(_msgSender() == transfer.from, "Unauthorized");
253
254
executedConditions[transfer.conditionHash] = true;
255
balances[transfer.from] -= transfer.amount;
256
balances[transfer.to] += transfer.amount;
257
}
258
}
259
```
260
261
### Gasless NFT Minting
262
263
```solidity
264
pragma solidity ^0.8.0;
265
266
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
267
import "@openzeppelin/contracts/metatx/ERC2771Context.sol";
268
import "@openzeppelin/contracts/utils/Counters.sol";
269
270
contract GaslessNFT is ERC721, ERC2771Context {
271
using Counters for Counters.Counter;
272
Counters.Counter private _tokenIds;
273
274
mapping(address => bool) public hasMinted;
275
276
constructor(address trustedForwarder)
277
ERC721("GaslessNFT", "GNFT")
278
ERC2771Context(trustedForwarder)
279
{}
280
281
function mint() external {
282
address user = _msgSender();
283
require(!hasMinted[user], "Already minted");
284
285
_tokenIds.increment();
286
uint256 tokenId = _tokenIds.current();
287
288
hasMinted[user] = true;
289
_safeMint(user, tokenId);
290
}
291
292
function _msgSender() internal view virtual override(Context, ERC2771Context) returns (address) {
293
return ERC2771Context._msgSender();
294
}
295
296
function _msgData() internal view virtual override(Context, ERC2771Context) returns (bytes calldata) {
297
return ERC2771Context._msgData();
298
}
299
}
300
```
301
302
## Meta-Transaction Best Practices
303
304
1. **Trusted Forwarders**: Only use well-audited forwarder contracts
305
2. **Nonce Management**: Implement proper nonce tracking to prevent replay attacks
306
3. **Signature Verification**: Always verify signatures before execution
307
4. **Gas Limits**: Set appropriate gas limits for forwarded transactions
308
5. **Deadline Protection**: Include deadlines to prevent stale transaction execution
309
6. **Cost Considerations**: Factor in the additional gas costs of meta-transactions
310
311
## Integration with Existing Contracts
312
313
### Upgrading to Meta-Transaction Support
314
315
```solidity
316
// Before: Regular contract
317
contract RegularContract {
318
function doSomething() external {
319
// msg.sender is the actual caller
320
require(msg.sender == owner, "Not owner");
321
}
322
}
323
324
// After: Meta-transaction enabled contract
325
contract MetaEnabledContract is ERC2771Context {
326
constructor(address trustedForwarder) ERC2771Context(trustedForwarder) {}
327
328
function doSomething() external {
329
// _msgSender() works for both regular and meta-transactions
330
require(_msgSender() == owner, "Not owner");
331
}
332
}
333
```
334
335
### Relayer Infrastructure
336
337
Meta-transactions typically require relayer services that:
338
339
1. Accept signed meta-transactions from users
340
2. Pay gas fees to execute transactions on-chain
341
3. Potentially charge fees or use other monetization strategies
342
4. Provide APIs for dApp integration
343
344
## Security Considerations
345
346
1. **Signature Replay**: Implement proper nonce mechanisms
347
2. **Forwarder Trust**: Only trust audited forwarder contracts
348
3. **Gas Griefing**: Implement gas limit controls
349
4. **Fee Extraction**: Be aware of MEV and fee extraction risks
350
5. **Contract Upgrades**: Consider meta-transaction compatibility in upgrades
351
352
## Error Handling
353
354
Meta-transaction contracts may revert with various errors:
355
356
- **MinimalForwarder**: Signature verification failures, nonce mismatches, insufficient gas
357
- **ERC2771Context**: No specific errors, but underlying contract logic may fail
358
- **General**: All standard contract errors apply, plus meta-transaction specific validation failures