Trusted Forwarder (ERC-2771 Forwarder)
The trusted forwarder is the on-chain component that makes ERC-2771 work. It sits between the relayer and your recipient contract and enforces the core security properties:
- The request was signed by the user (
from) - The request is not replayed (nonce)
- The request is still valid (deadline)
- The recipient contract trusts this forwarder
After verification, the forwarder calls the target contract and appends the
original sender address to calldata, allowing the recipient to recover it via
ERC2771Context.
What the forwarder verifies
In OpenZeppelin’s ERC-2771 implementation, the request contains:
from: original user addressto: recipient contract addressvalue: native token value to forward (often0)gas: gas limit for the forwarded callnonce: anti-replay counter forfromdeadline: request expirydata: encoded function call for the recipient
The forwarder verifies the signature over these fields using EIP-712.
Concrete request shape (typed data)
From the working Watr demo in this repo, the EIP-712 typed data ForwardRequest
fields are:
export const FORWARD_REQUEST_TYPES = {
ForwardRequest: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "gas", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint48" },
{ name: "data", type: "bytes" }
]
};
How the “real sender” is propagated
When the forwarder calls the recipient, it uses:
- The original call data (
request.data) - Plus the original sender (
request.from) appended to the end of calldata
On the recipient side, ERC2771Context only trusts and decodes this extra
sender data when msg.sender is the configured trusted forwarder.
Repo-specific note: forwarder name must match EIP-712 domain
In this codebase, the forwarder is deployed as a thin wrapper around
OpenZeppelin’s ERC2771Forwarder. The forwarder name is part of the EIP-712
domain, so the frontend/backends signing logic must use the same name as the
deployed forwarder.
If the name does not match, signature verification will fail even if all other fields are correct.
Example from the reference relay script in watr-gasless-transactions/backend/scripts/relay.ts:
const domain = {
name: "MyForwarder",
version: "1",
chainId,
verifyingContract: forwarderAddress
};