DeFi Security AllianceRequest an audit
Menu

EVM patterns

ERC2771 Forwarder Security: Trusted Context and Request Evidence

A recipient reads an appended sender only through a trusted forwarder with sufficient calldata. ERC2771 forwarder security requires authentic request verification, correct context consumption and application authorization on every relevant route.

Blue envelope with an attached transparent identity card entering a white inspection gateway, illustrating trusted forwarded context.

Key facts

Original recipient grid
32 labeled logical cases; 28 distinct caller/calldata/consumer inputs
Context consumers
4 of 16 read suffix; 12 fall back to caller
Raw consumers
16 of 16 retain actual caller identity
Pinned signer scheme
OpenZeppelin v5.4.0 forwarder uses ECDSA, not ERC1271
Limit
Interpretation model, no signature recovery, EVM or target call

Separate the relay from the effective sender

The transaction submitter, forwarder and effective sender can be different actors. ERC2771 forwarder security requires the recipient to interpret an appended signer address only when the actual caller is trusted, while the forwarder authenticates the request. A suffix that resembles an address is not evidence of authorization by itself.

A trusted forwarder is a contract that a recipient relies on to verify signed requests and nonces. It appends the signer's address to calldata before calling that recipient. The recipient checks the caller relationship before treating that suffix as the effective sender. The trust decision authorizes an interpretation of bytes; the request verification establishes a different part of the evidence.

The distinction is visible in source. OpenZeppelin's pinned ERC2771Context v5.4.0 extracts a sender only when the caller is trusted and calldata contains a complete context suffix. Otherwise it preserves ordinary msg.sender. Its paired forwarder verifies an EIP712 request using ECDSA recovery. These are specific implementations, not a universal claim about every ERC2771 forwarder's supported signature schemes.

Identity observations in a relayed call
Observed actorMeaningSecurity boundary
Transaction submitterThe actor paying to submit the transactionNot automatically the authorized application user
Recipient callerThe immediate contract invoking the recipientMust be trusted before suffix interpretation
Appended senderIdentity supplied through the trusted contextDepends on the forwarder's authentic request check
Raw sender consumerCode reading ordinary msg.senderCan see the forwarder instead of the user

The ERC2771 specification leaves the recipient's trust implementation open. It warns that a malicious trusted forwarder can forge the perceived sender and that an upgradeable forwarder's authority belongs in the trust analysis. Reviewing only the current signer recovery function would omit that deployment dependency.

Our signature replay guide explains the broader authorization-message problem. This article follows the evidence into the recipient: which sender each path reads, how the forwarder binds requests and what happens during batching or delegated execution.

A successful forwarder verification is not a successful business action. The target may reject its own preconditions or execute an action whose effects need independent checks. Keep request validity, low-level call outcome and application result as separate observations throughout testing and logging.

Original recipient context grid

Method

We modeled the pinned recipient's context interpretation over a complete labeled input grid. The caller was trusted or untrusted, calldata had a selected length, a synthetic suffix identity was supplied and the consuming path read either raw or context-aware sender. No signature recovery or application function executed.

Source
ERC2771 and OpenZeppelin Contracts v5.4.0 ERC2771Context.sol, retrieved on .
Axes
Both caller-trust states, lengths 0, 19, 20 and 36 bytes, two suffix labels and raw/context-aware consumers.
Population
32 labeled Cartesian cases, with no exclusions. There are 28 distinct caller/calldata/consumer encodings because empty payloads cannot distinguish the two suffix labels.
Scope
Sender interpretation only. The byte strings are not asserted to be valid ABI-dispatched business calls.
Artifacts
seo/research/integration-boundaries-2026-10-11/support-execution/finite-models.py and erc2771-forwarder-security-model-2026-10-11.json, plus all CSV rows. These repository artifacts are not public downloads.

Results

Complete labeled sender-interpretation grid, measured
Consumer observationCasesDenominator
Context consumer reads suffix416 context-aware cases
Context consumer falls back to caller1216 context-aware cases
Raw consumer reads caller1616 raw-consumer cases

Only trusted callers with a complete suffix produced extraction. Length 19 exercised the short-payload fallback; length 20 exercised equality. Length 36 showed that the sender remains at the end of a longer payload. A complete suffix from an untrusted caller remained ordinary calldata.

The full labeled population contains 8 cases where raw and context-aware readings differ under the corresponding trust and length inputs. That difference identifies a code-review boundary. It is not a count of access-control defects, since an application can intentionally choose a particular identity for a particular operation.

Limits

  • No EVM, signature verifier or target function ran. This is a finite source-derived interpretation model.
  • The labeled denominator preserves the declared factors, including duplicate empty-payload encodings. The distinct-input count is reported separately.
  • Suffix extraction does not establish authenticated signing, permitted business action or successful target execution.

The practical test is therefore two-part: establish why the recipient trusts the forwarder, then verify that every relevant application path reads the intended identity. A correct extraction helper cannot repair a privileged function that bypasses it.

Bind the forwarded request before execution

OpenZeppelin's pinned forwarder signs a request containing sender, target, native value, gas, nonce, deadline and data. It reads the current nonce from on-chain state when hashing. Its request verification checks recipient trust, deadline validity and matching recovered signer. Those checks approve the request boundary, not the target's resulting business state.

The source accepts a deadline equal to the current timestamp. An expiry test should exercise equality separately from the first expired value. Do not assert a stricter predicate because a frontend countdown rounded the displayed time differently. The authorization boundary belongs to the deployed contract.

Verified request and recipient identity form separate evidenceThe forwarder verifies request fields, nonce and deadline. The recipient trusts the immediate caller before extracting the appended sender. The business function then applies its own authorization and state conditions.Request verificationTrusted contextBusiness rulesEach layer needs its own acceptance evidence
The recipient's extracted sender is an input to application authorization, not its final verdict.
Request verification
The forwarder checks its actual signed fields, nonce, deadline and recipient trust.
Context interpretation
The recipient validates caller trust and sufficient calldata before reading the suffix.
Application authorization
The target function checks that the effective actor may perform the requested action in current state.

This fixed forwarder uses ECDSA and does not call ERC1271. A contract wallet supported by another application is not automatically supported as a signer here. ERC2771 allows different forwarder designs, so the compatibility restriction belongs to the pinned implementation. Verify the actual signing population before promising contract-wallet support.

Single execution requires exact equality between supplied native value and the request's value. A valid request consumes its nonce before the target call. If the single target call returns failure, the outer execution reverts, restoring the nonce with the transaction state. Those source-level relationships need implementation tests in the actual integration.

Recipient trust is queried through a static call. OpenZeppelin's pinned forwarder's discovery predicate requires successful return, sufficient response data and a nonzero returned word. Record both the discovery observation and the recipient's configured trust. A provider failure should not silently become evidence that a recipient deliberately rejected the forwarder.

Trust-list updates and forwarder upgrades need a change owner. The standard does not prescribe one trust-management system. A mutable list, an immutable configured forwarder or an overridden trust function has different authority paths. Inspect the actual implementation and who can change the behavior users depend on.

Preserve the actual signing domain in that record. OpenZeppelin's pinned forwarder uses its supplied name and domain version 1 in EIP712 construction. A request assembled for another deployment or domain needs its own binding check. Similar request fields do not establish interchangeable authorization. Compare the displayed action, encoded domain and on-chain digest before approving compatibility between two relay integrations.

Inspect batching and delegated context

Forwarded context can become corrupted if an application self-delegates arbitrary calldata and then treats its tail as the original signer. The pinned context source warns about forwarded self-delegatecall. A reviewer must trace the suffix through nested execution, rather than assume that a correct outer extraction makes every inner call safe.

The pinned Multicall implementation handles a noncanonical context by appending the original suffix to its delegated subcalls. That is a specific integration pattern. Arbitrary custom self-delegatecalls need their own context-preservation argument. Copying one of these components without checking how the other constructs calldata is incomplete.

Batch and delegated execution boundaries
BoundaryRequired inspectionUnsupported shortcut
Nested sender recoveryOriginal context suffix reaches intended subcallOuter extraction passed
Nested action filteringActual inner targets and selectors are permittedOuter multicall selector was allowed
Batch native valueSupplied value equals request-value sumEach individual request looked valid
Failure and refund behaviorNonce, value and target state after each failure pathA variable is named atomic

OpenZeppelin's Multicall documentation states that an outer selector filter does not automatically filter nested selectors. A relay service allowing one batch function may consequently allow more application actions than its top-level policy suggests. Review nested content and the recipient's own restrictions. A convenient transport wrapper should not broaden business permission unnoticed.

OpenZeppelin's pinned forwarder batch enforces aggregate native-value equality. A nonzero refund receiver permits skipping invalid requests instead of immediately reverting them. A zero refund receiver selects the path requiring valid requests. Keep that validity condition separate from the outcome of an already valid target call.

There is a source-comment boundary worth preserving in a review. In v5.4.0, the batch variable named atomic controls request-validity enforcement, while the loop accumulates value when execution reports failure. The loop does not itself unconditionally revert solely because a valid top-level target call returns false. Do not approve an all-or-nothing target rollback guarantee from the variable name or broad comment alone.

This is a bounded source observation, not an executed vulnerability finding. The integration needs a regression test for valid-request target failure, invalid-request handling, nonce state and refund behavior under its actual configuration. Other checks or nested behavior may affect the final transaction outcome; source interpretation should not be reported as an observed exploit.

Pharos Production describes reviewing access-control gaps and logic flaws in its smart contract authorization and execution-logic audit service. That scope is semantically close to trusted-context consumption and nested action checks. Specify the forwarder, recipient and batching composition in the engagement; the service page does not claim that a particular ERC2771 deployment has already been reviewed.

Test rejection and recipient failure separately

A rejection test should preserve the surrounding valid conditions. Start with a request the intended signer, forwarder and recipient accept. Then change caller trust, suffix length, nonce, deadline or signed action separately. Inspect both the rejecting gate and the state that remains afterward.

  1. Pin the forwarder and recipient implementations, trust configuration and supported signer scheme.
  2. Send an untrusted caller's appended address and confirm the recipient keeps ordinary caller identity. Repeat a trusted call with a short payload to exercise the length boundary.
  3. Execute a valid forwarded action through every relevant mutation route. Verify that its access control consumes the intended identity.
  4. Change a material signed field, reuse a consumed nonce and cross the deadline boundary with otherwise accepted fixtures.
  5. Test single target failure, invalid batch requests and valid-request target failure separately. Inspect nonce state, value movement and refunds.
  6. Exercise nested selectors and self-delegatecalls with context-sensitive authorization. Preserve positive controls showing intended composition still works.

Pay attention to raw calldata-length checks. OpenZeppelin's context implementation warns that appending a suffix can affect code that selects behavior based on exact calldata length, including receive or fallback paths. A gas-sponsored call that appears empty at the application level is not necessarily empty in raw forwarded bytes. Test the actual recipient dispatch.

A native refund is also a call surface. A refund receiver can affect control flow according to the actual implementation. Specify who receives refunds and test its failure behavior in the deployed batch path. The context grid contains no value movement and cannot establish this property.

Gas delivery has a separate source check measured immediately after the target call. Its presence does not establish that the target's business action succeeded. The operator should record the requested gas, low-level outcome and interpreted application result separately. A successful request-verification API can precede a target that rejects current state.

The transaction simulation guide explains how to preserve state and input assumptions for these rehearsals. A simulation should use the actual forwarding route rather than call the recipient directly as the signer. The direct call would bypass the context transformation under review.

A native refund can arrive with empty calldata. OpenZeppelin's pinned forwarder describes that call shape, so a refund recipient that also trusts the forwarder needs to handle length zero correctly. The current context helper falls back to the actual caller in that case. A trust relationship does not create a nonexistent sender suffix.

Use a recipient that deliberately rejects the refund as a separate failure fixture. Inspect the outer transaction outcome, refund amount and nonce state rather than assuming that a failed target and failed refund have the same result. These are proposed integration tests; the sender-interpretation grid moved no value.

Source-derived batch distinctions requiring execution tests
Request pathNonce boundaryRemaining condition
Invalid request allowed to skipRejected before valid-request nonce consumptionOuter batch and refund behavior
Valid request and successful targetNonce consumed before target callApplication result and any later outer revert
Valid request and failed targetNonce consumption was reachedFinal persistence depends on enclosing outcome

The valid-failed row should be retested using the resulting nonce only after establishing the outer outcome. If a later outer revert restores state, an intermediate source step is not evidence that the nonce remained consumed. If the outer transaction succeeds, inspect the stored nonce directly. The evidence needs to connect the control-flow observation to the final committed state.

Also review the trust method itself. The pinned context stores a configured immutable forwarder, but its virtual trust accessors can be overridden in a composed recipient. An immutable field alone does not establish that every derived trust decision is fixed. Preserve the actual recipient implementation and its override paths in the trust record.

Record the trust boundary before enabling relays

The acceptance record should identify supported signers, signed fields, nonce and deadline behavior, recipient trust and the identity consumed by privileged routes. Include batching and delegated context tests where they exist. Separate request validity from target success and application invariants in the public status model.

The security firm directory and Pharos Production member profile support selecting a reviewer. Give that reviewer the full relay composition and its authority configuration. A standalone ECDSA check cannot establish that the recipient interprets the suffix correctly or that nested actions remain within policy.

Our original model establishes an interpretation result: 4 of 16 context-aware labeled cases read the suffix, while the others fall back. The distinct-input count and model limits remain attached to that result. It should not be translated into a claim that only a certain fraction of real requests are secure.

Enable the relay only when the evidence connects request authentication, trusted context and business authorization. Revisit that connection after a trust update, forwarder upgrade or new batching path. The effective sender is useful because its provenance is explicit, and that provenance needs to survive the entire execution route.

Frequently asked questions

What does ERC2771 require of trust discovery gas?

Its isTrustedForwarder discovery method must not revert and should use at most 50000 gas. The gas language is SHOULD, not a mandatory universal transaction budget.

Can one recipient support different forwarding formats?

The standard permits trusted forwarders with different signing and batching formats. Each trusted implementation still needs its own request and authority review.

Does adding ERC2771 create a privacy guarantee?

The interface changes how a recipient interprets an authenticated sender context. It does not establish anonymity, unlinkability or a privacy property for the application.

Comments

0

    Leave a comment

    Share a question or observation about this article.

    10 to 3,000 characters.