EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8339: Receiver-Attested Transfers

ETH and token transfers that execute only if the receiver signed consent for exactly that transfer, checked inside one EIP-8141 frame transaction.

Authors
Created2026-07-12
Discussion Linkhttps://ethereum-magicians.org/t/erc-8339-two-phase-asset-transfers/29017
Requires

Markdown

https://raw.githubusercontent.com/ethereum/ERCs/re...
Pull Request#1882PR open

EIP-GPT summary

Contents
AbstractMotivationThe receiver should have a say, before anything movesWhy the address must come from the receiverSpecificationDefinitionsFrame layoutTransferConsentGuard rulesTransfer frame matchingSingle useIFrameConsentGuardOff-chain messagesWallet rulesRationaleWhy frames instead of an escrowWhy the challenge replaces the secretWhy the consent is single-useWhy the guard is a DEFAULT frame with ATOMICBATCHFLAGWhy the guard reads the transfer frameWhy exact calldata matchingWhy the request carries no receiver, and why the transport is a messengerEIP-712 and ERC-1271Trade-offsBackwards CompatibilityReference ImplementationSecurity ConsiderationsNo path for a wrong addressReplayWhat observers seeThe atomic flag is load-bearingIntrospection trustNon-standard tokensOut of scope: compromised conversationsGriefingTimestampsCopyright

Abstract

This ERC specifies receiver-attested transfers. A send of native ETH or any ERC-20, ERC-721, or ERC-1155 asset executes only if the receiver has signed consent for exactly that transfer.

The receiver signs first, off-chain, an EIP-712 TransferConsent naming their own address, the asset, the amount, a sender-chosen challenge and a deadline. The sender then submits one EIP-8141 frame transaction with three frames: the sender's own authorization, a guard frame that checks the consent, and the transfer frame that moves the asset. The guard reads the transfer frame itself and reverts unless it is exactly the consented transfer. The guard frame and the transfer frame form an atomic batch, so a mismatch skips the transfer and nothing moves.

There is no escrow, no deposit, no pending state and no second transaction. The receiver's address comes from the receiver, inside the signed consent; the sender never types it. A mistyped, poisoned or swapped address cannot produce a valid consent, so it cannot receive anything. Each consent pays out at most once.

The standard defines the guard interface (IFrameConsentGuard), the frame layout, the consent format and the two off-chain messages. It does not define a transport: the messages travel over whatever private, censorship-resistant channel the two people already share. No asset contract needs to change. The guard is discoverable via ERC-165.

Motivation

On Ethereum, a transfer cannot be undone. Send ETH or tokens to the wrong address (one mistyped character, a poisoned clipboard, a swapped QR code, a contact who lost that key) and the assets are gone. Analytics firms report hundreds of millions of dollars locked in unreachable addresses every year. This is not a flaw of any one token standard; it is how push-based sending works everywhere.

The receiver should have a say, before anything moves

In a normal transfer, only the sender acts. The receiver does nothing, and a wrong address owns the funds the instant the transaction lands. Escrow designs fix this by parking the funds and letting the receiver accept later, but that costs two transactions, an approval step, a custodian contract holding everyone's pending value, and a pending state that wallets, indexers and users must track.

EIP-8141 frame transactions make the escrow unnecessary. One transaction can carry several frames, and each frame can read the others. So the receiver's consent can be checked in the same transaction as the send, against the actual transfer the transaction is about to execute. The receiver acts first, the sender sends once, and either the exact consented transfer happens or nothing does.

Why the address must come from the receiver

Asking the receiver to sign does not help if the sender asks the wrong person. If a wallet sent the consent request to the typed address, a typo would deliver the request to the stranger at that address, the stranger would sign, and the funds would go to them.

So the request carries no receiver at all. It goes to the person, over the conversation the two already have, and the receiver answers by naming and signing for their own address. The address the sender pays is the address whose key produced the signature. There is no typed address left to get wrong.

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.

Definitions

  • Consent: an EIP-712 TransferConsent signed by the receiver, authorizing one transfer of one asset from one sender to the receiver before a deadline.
  • Consent digest: the EIP-712 hash of a consent under the guard's domain. It is both what the receiver signs and the key under which the guard records that the consent was used.
  • Challenge: 32 bytes generated at random by the sender's wallet, fresh for every request. It makes every consent unique. It is not secret.
  • Guard: a contract implementing IFrameConsentGuard.
  • Guard frame: the EIP-8141 frame that calls check on the guard.
  • Transfer frame: the frame immediately after the guard frame, which moves the asset.

Frame layout

A receiver-attested transfer is one EIP-8141 frame transaction whose sender is the consent's sender, containing at least these frames, in this order:

Framemodeflagstargetdata
sender authorizationVERIFYapproves execution and paymentsenderper the sender's account
guard frame (index g)DEFAULTATOMIC_BATCH_FLAG setguardcheck(consent, receiverSig)
transfer frame (index g + 1)SENDERnonesee matching rulessee matching rules

With the sender's validation prefix being [self_verify], this layout is accepted by the public mempool rules of EIP-8141. If the guard frame reverts, the atomic batch skips the transfer frame.

TransferConsent

TransferConsent(address sender,address receiver,uint8 kind,address token,uint256 tokenId,uint256 amount,bytes32 challenge,uint64 deadline)

The EIP-712 domain MUST include name, version, chainId and verifyingContract (the guard). kind is 0 Native, 1 ERC-20, 2 ERC-721, 3 ERC-1155. Field consistency:

  • Native: token == address(0), tokenId == 0, amount > 0.
  • ERC-20: token != address(0), tokenId == 0, amount > 0.
  • ERC-721: token != address(0), amount == 1.
  • ERC-1155: token != address(0), amount > 0.
  • All kinds: receiver != address(0) and receiver != sender.

Guard rules

check(c, receiverSig) MUST revert unless all of the following hold, and MUST evaluate the sender check before reading or writing the used record:

  1. The current frame has ATOMIC_BATCH_FLAG set (NotAtomic). Without it a guard revert would not stop the transfer frame, and the guard would protect nothing.
  2. The next frame exists and its mode is SENDER (BadTransferFrame).
  3. TXPARAM(SENDER) == c.sender (WrongSender).
  4. block.timestamp <= c.deadline (ConsentExpired).
  5. The consent fields are consistent with c.kind (BadAsset).
  6. The next frame matches the consent per the matching rules below (BadTransferFrame). The guard MUST read the target, value and data of the next frame itself, via the EIP-8141 introspection opcodes. It MUST NOT trust any copy of them passed as an argument.
  7. The consent digest has not been used before (AlreadyUsed).
  8. receiverSig is a valid signature of the consent digest by c.receiver: ECDSA for an account without code, ERC-1271 isValidSignature for an account with code (BadConsent).

On success the guard MUST record the digest as used and emit ConsentUsed. Because the guard frame and the transfer frame are one atomic batch, the record is rolled back if the transfer frame fails, and the consent stays usable.

Transfer frame matching

target, value and data are the next frame's resolved target, value and data.

  • Native: target == c.receiver, value == c.amount, data empty.
  • ERC-20: target == c.token, value == 0, data exactly abi.encodeCall(IERC20.transfer, (c.receiver, c.amount)).
  • ERC-721 / ERC-1155: target == c.token, value == 0, and data exactly one of the encodings listed in the reference implementation's _matchNftCall, moving c.tokenId (and for ERC-1155, c.amount) from c.sender to c.receiver.

Matching is on exact bytes. Anything else, including extra or trailing data, MUST NOT match.

Single use

A consent MUST pay out at most once per guard. The guard MUST keep the set of used digests and MUST expose it through isUsed. A recurring payment uses a new consent, with a new challenge, each time.

IFrameConsentGuard

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.20;

interface IFrameConsentGuard {
    enum AssetKind { Native, ERC20, ERC721, ERC1155 }

    struct Consent {
        address   sender;
        address   receiver;
        AssetKind kind;
        address   token;
        uint256   tokenId;
        uint256   amount;
        bytes32   challenge;
        uint64    deadline;
    }

    event ConsentUsed(bytes32 indexed digest, address indexed sender, address indexed receiver);

    error NotAtomic();
    error BadTransferFrame();
    error WrongSender();
    error ConsentExpired();
    error AlreadyUsed();
    error BadConsent();
    error BadAsset();

    function check(Consent calldata c, bytes calldata receiverSig) external;
    function consentDigest(Consent calldata c) external view returns (bytes32);
    function isUsed(bytes32 digest) external view returns (bool);
}

Guards MUST support ERC-165 and return true for type(IFrameConsentGuard).interfaceId.

Off-chain messages

Two messages travel between the wallets. This ERC fixes their bytes, not their transport.

message  = version (1 byte, 0x01) || type (1 byte) || abi.encode(body fields, in order)

type 0x01 ConsentRequest  (sender -> receiver)
  body = (uint256 chainId, address guard, address sender, uint8 kind, address token,
          uint256 tokenId, uint256 amount, bytes32 challenge, uint64 deadline)

type 0x02 ConsentResponse (receiver -> sender)
  body = (address receiver, bytes sig)

A ConsentRequest has no receiver field. The receiver's wallet builds the TransferConsent from the request plus the account the receiver chooses to receive on, signs it, and returns that account and signature in a ConsentResponse.

Wallet rules

Sending wallets:

  • MUST generate a fresh 256-bit random challenge for every request.
  • MUST deliver the request to the person, over a channel already associated with them (an existing conversation, a contact key, a session). MUST NOT address the request to a typed or pasted Ethereum address.
  • MUST take the transfer's receiver from the verified response, and MUST verify the signature locally (ECDSA or ERC-1271 on the target chain) before building the transaction.
  • SHOULD show the receiver address next to the conversation it came from.
  • SHOULD check isUsed(consentDigest(c)) before sending.
  • SHOULD use end-to-end encrypted, censorship-resistant messaging. Consent messages SHOULD NOT pass through any server that can read, block or alter them.

Receiving wallets:

  • MUST show the sender, asset, amount and deadline before signing.
  • MUST sign only for an account the user controls on the request's chain.
  • MUST check the guard via ERC-165 on that chain before signing.

Rationale

Why frames instead of an escrow

An earlier version of this draft standardized an escrow: the sender deposited, the receiver accepted later with two factors, and the sender could revoke or reclaim meanwhile. The deposit existed only because the receiver acted after the send. With EIP-8141 the receiver can act before the send, and the check can run in the same transaction as the transfer. That removes the second transaction, the approval step, the custodian contract, the pending state, revoke and reclaim, and the honeypot of pooled pending value. The guard holds no assets at all.

Why the challenge replaces the secret

The escrow needed a sender-generated secret to stop one case: an active stranger at a wrong address seeing the pending transfer and accepting it first. Here there is nothing pending to see, and the stranger cannot produce a consent for an address they do not control or for a request they never received. What remains for the sender's random value to do is make every consent unique, so that the single-use record can tell two payments apart. That is the challenge. It need not be secret, and it appears on-chain in the guard frame.

Third-party replay is already impossible without any guard state (see Security Considerations). The used record exists for the sender's own mistakes: a wallet glitch or a double click that sends the same consent again in a new transaction. Without it, the second payment would still go to the right receiver, but it would be a payment the receiver never freshly confirmed, and an invoice could be paid twice. One consent, one payment, enforced on-chain, costs one storage write.

Why the guard is a DEFAULT frame with ATOMIC_BATCH_FLAG

A VERIFY frame that does not approve would make a mismatched transaction invalid rather than reverted, so the sender would not even pay gas. But EIP-8141's public mempool rules accept only recognized validation prefixes, and a non-approving VERIFY frame is recognized only for the canonical expiry verifier. A DEFAULT guard frame after the prefix keeps the transaction propagatable for plain EOAs, and ATOMIC_BATCH_FLAG gives the property that matters: if the guard reverts, the transfer frame does not run. Implementations MAY offer a strict VERIFY variant for private orderflow.

Why the guard reads the transfer frame

If the guard compared the consent to arguments, a buggy or compromised wallet could pass the right arguments and build a transfer to a different address. Reading the next frame's target, value and calldata with the EIP-8141 introspection opcodes ties the check to what actually executes. The guard also checks its own ATOMIC_BATCH_FLAG and that the next frame runs in SENDER mode, because either one missing would leave the transfer unprotected.

Why exact calldata matching

Accepting any call that "looks like" a transfer would let extra arguments, other selectors or trailing bytes slip through. Exact bytes over a short list of encodings keep the guard small and the property simple: the transfer frame is one of the listed encodings of exactly the consented transfer, or nothing moves.

Why the request carries no receiver, and why the transport is a messenger

If the request named a receiver, wallets would be tempted to send it to that address, for example over address-based messaging, and a typo would hand the request to a stranger. With no receiver in the request, the only way to learn who to pay is the signed answer, and the only place to send the request is the person.

The messages are not secret. A response works only for its one sender, asset, amount, chain, guard and deadline. So any relay can carry them, and none needs to be trusted. That is why this ERC fixes bytes and not a server. Private, end-to-end encrypted, censorship-resistant messaging is RECOMMENDED: it keeps who-pays-whom off public servers before the transaction, and no operator can block a payment by refusing to relay the consent.

EIP-712 and ERC-1271

EIP-712 gives receivers a readable signing prompt and binds the consent to one chain and one guard through the domain. ERC-1271 lets smart-account receivers consent. Checking the signature on the target chain also catches "same address, wrong chain": a smart account that is not deployed there, or has a different owner there, cannot produce a valid signature.

Trade-offs

  • The receiver must answer before the sender can send. Sending to someone who is offline has to wait until they are back.
  • The design requires EIP-8141. Chains without frame transactions are not covered.
  • Tokens that return false instead of reverting, fee-on-transfer and rebasing tokens can make the transfer frame "succeed" without delivering the consented amount; see Security Considerations.

Backwards Compatibility

No asset contract changes. To the asset, the transfer frame is an ordinary transfer, transferFrom or safeTransferFrom call (or plain value send) from the sender, so indexers, DEXs and marketplaces see a normal transfer. Plain sends outside this flow are untouched.

This version supersedes the escrow-based draft of the same ERC. Escrow contracts already deployed from that draft keep working, but are not conforming implementations of this standard.

Reference Implementation

A reference implementation and a Foundry test suite are in the assets directory:

  • IFrameConsentGuard.sol: the interface above.
  • FrameConsentGuard.sol: the guard. EIP-712, ERC-1271 via OpenZeppelin SignatureChecker, ERC-165. No custody, no owner, no upgradability; its only state is the used record.
  • FrameIntrospector.yul with IFrameIntrospector.sol: a stateless wrapper exposing TXPARAM, FRAMEPARAM and FRAMEDATACOPY through verbatim, because Solidity cannot emit these opcodes.
  • ConsentCodec.sol: the canonical ConsentRequest and ConsentResponse bytes.
  • Tests run against MockFrameIntrospector, which describes a frame transaction on today's EVM. They cover native and ERC-20 happy paths, a typo'd receiver, a signature from the wrong key, a missing atomic flag, the wrong frame mode, expiry, third-party replay and consent burning, rollback of the used record on a failed transfer, and ERC-1271 receivers.

ERC-721 and ERC-1155 frame matching is not yet implemented in the reference implementation: _matchNftCall fails closed, so those transfers are always skipped until it lands.

Real frame execution requires an EIP-8141 client. The introspector compiles but is not exercised by the test suite.

Security Considerations

No path for a wrong address

  • Typed or pasted address. There is none. The receiver comes from the signed response, and the guard checks the signature against it.
  • Signature from another key. A consent naming X but signed by Y fails BadConsent.
  • Wallet builds the transfer to another address. The guard compares the real transfer frame to the consent and fails BadTransferFrame.
  • Same address, wrong chain. The consent is bound to chainId and the guard. A smart account that does not exist on the target chain, or has another owner there, fails ERC-1271.

In every case the guard reverts, the atomic batch skips the transfer frame, and nothing moves.

Replay

  • Rebroadcasting the sender's transaction. It carries the sender's nonce, which the protocol executes once.
  • Copying the consent into another transaction. The sender's authorization signs the EIP-8141 signature hash, which commits to every frame, the nonce and the chain, so nobody else can build or alter a transaction that runs as the sender. In the attacker's own transaction, TXPARAM(SENDER) != c.sender and the guard fails WrongSender. The SENDER transfer frame can only spend from the transaction's approved sender.
  • Front-running to burn a consent. The sender check runs before the used record is read or written, so an attacker cannot consume someone else's consent.
  • The sender's own second transaction. Fails AlreadyUsed.

What observers see

The guard frame's calldata, including the consent and the receiver's signature, is public once broadcast. That reveals who pays whom, as any transfer does, and nothing reusable: the signature works only for that one consent, which only that sender can use, once. Before broadcast, the consent travels only between the two wallets.

The atomic flag is load-bearing

A guard frame without ATOMIC_BATCH_FLAG, or one not directly followed by the SENDER transfer frame, would revert on its own while the transfer still ran. Guards MUST check both. Wallets MUST NOT construct the layout any other way.

Introspection trust

The guard's security rests on reading the real frames. Implementations that read frames through a helper contract MUST fix its address immutably at deployment, and that helper MUST be a stateless wrapper of the EIP-8141 introspection opcodes. Wallets SHOULD use only guards whose helper they have verified.

Non-standard tokens

The guard checks the transfer frame's calldata, not its effect. A token that returns false instead of reverting lets the batch succeed, so the consent is used up but nothing is delivered. Fee-on-transfer and rebasing tokens deliver an amount different from the consent. Such tokens are out of scope. Wallets SHOULD simulate the transaction and SHOULD NOT use this flow with them.

Out of scope: compromised conversations

This ERC protects against mistakes. If the conversation itself is compromised, so that an attacker answers the request and names their own address, the sender pays the attacker. No contract can tell that apart from the real receiver. Wallets SHOULD show where a response came from.

Griefing

Anyone can send a request. It costs nothing on-chain and moves nothing unless the receiver signs and the sender sends. Receiving wallets SHOULD let users ignore requests from unknown contacts.

Timestamps

deadline uses block.timestamp. Validator drift of seconds is negligible against any reasonable deadline.

Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth