or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

access-control.mdcrosschain.mdfinance.mdgovernance.mdindex.mdmetatx.mdproxy.mdsecurity.mdtokens.mdutilities.md

metatx.mddocs/

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