Skip to main content
In this guide you make a contract accept gasless calls, then sign a request from a wallet with 0 tBOT and relay it so another wallet pays the gas.
Everything on this page uses testnet (chain 968). Get free test tokens from the faucet.

How it works

ERC-2771 splits a call into two parts:
  • The user signs a ForwardRequest as EIP-712 typed data: “call this function on this contract for me”.
  • A relayer sends that request to a forwarder contract in a transaction it pays for.
The forwarder checks the signature, the user’s nonce and the deadline, then calls your contract with the user’s address appended to the calldata. Your contract extends OpenZeppelin’s ERC2771Context, whose _msgSender() returns that address when the caller is the trusted forwarder. Each nonce works once, so a signed request can’t be replayed.

What you’ll build

  • A forwarder and a small Notes contract that records who set each note.
  • A viem script that creates a new wallet with no tBOT, signs a request, and relays it from your funded wallet.

Prerequisites

  • A Foundry project from Set up Foundry, with the uzo-dev keystore and some tBOT.
  • The bot-defi project and .env file from Wrap BOT, for the script.

Steps

1

Install OpenZeppelin

In your Foundry project:
Add @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ to remappings in foundry.toml if it isn’t there.
2

Write the forwarder

Use OpenZeppelin’s forwarder unchanged. The name becomes part of the EIP-712 domain that wallets show when signing.
src/Forwarder.sol
3

Make your contract trust it

Extend ERC2771Context, pass the forwarder’s address to its constructor, and use _msgSender() wherever you’d use msg.sender.
src/Notes.sol
Any msg.sender left in your contract sees the forwarder, not the user. Check every one, including those in inherited contracts such as Ownable, which already use _msgSender() in OpenZeppelin 5.
4

Deploy both

Save both addresses. To verify them, see Verify with Foundry.On 2026-10-02 these deployed a forwarder and Notes on testnet. Running the next step’s script against them, with setNote in place of sign, stored the note under the signing user’s address, not the relayer’s.
5

Sign and relay a request

This script plays both roles. A brand new wallet signs, and the wallet in your .env relays. It uses a forwarder and guest book deployed on testnet by the gasless app template. To use your own contracts, change the two addresses and the function.
meta-tx.ts
6

Understand each part

  • The domain comes from the forwarder’s eip712Domain(), so the name, version, chain ID and address always match what the forwarder checks.
  • The nonce comes from nonces(user). It’s part of the signed message but not of ForwardRequestData, because the forwarder looks it up itself. That’s why the script removes it before calling verify and execute.
  • gas is the gas the forwarder passes to your contract. If it’s too low, the inner call fails.
  • deadline is a Unix time in seconds. After it, the request is rejected.
  • verify checks the signature, nonce, deadline and that the target trusts this forwarder, without spending gas.
7

Run it

Verify it worked

On testnet on 2026-10-01:
Output
Open the transaction on BOTScan. It was sent by your relayer wallet, to the forwarder. The new guest book entry’s author is the user address, which still holds 0 tBOT. Read it with getEntries, as in Events without getLogs.

Sign in a browser wallet

In a web app, the user signs with their own wallet instead of a generated key. With wagmi, use signTypedData with the same domain, types and message. The wallet shows the request fields and asks for a signature, not a transaction. Then post the request to your relayer. The gasless app template does this end to end.

Troubleshooting

The signature doesn’t match the request. Check you signed with the forwarder’s own domain, used the current nonce, and sent exactly the fields you signed. A request that already ran fails this way too, because its nonce is used.
The deadline passed. Sign again.
Your contract doesn’t trust this forwarder. Deploy it with the forwarder’s address in the constructor.
The forwarder’s call to your contract reverted. Call your function directly with the same arguments to see why, or raise gas.
The function uses msg.sender. Change it to _msgSender().

Next steps

Run a relayer

Accept signed requests over HTTP, safely.

Gasless app template

Contracts, relayer and web app.
Last modified on October 2, 2026