Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7946: Unidirectional Wallet Uplink aka UWULink

Unidirectional app to wallet communication for private construction of transactions

Authors Moody Salem (@moodysalem), Tina Zheng (@tinaszheng)
Created 2025-05-10
Discussion Link https://ethereum-magicians.org/t/erc-7946-unidirectional-wallet-uplink-aka-uwulink/24282
Requires EIP-155, EIP-7702

Abstract

Universal Wallet Uplink (UWULink) is a protocol that allows applications (dApps) to request a wallet to make a batch of contract calls in a single atomic transaction without establishing a two-way connection or revealing the user’s address to the requester. The protocol defines a compact binary message format using Protocol Buffers suitable for low-bandwidth channels such as QR codes or NFC tags. Two modes of operation are supported: Static Mode, where the request payload directly contains the list of calls, and Programmable Mode, where the payload references a contract that generates the list of calls.

Motivation

dApps often require users to perform multistep interactions, such as approving a token and then executing a swap. Traditionally, accomplishing this has required multiple user confirmations or complex wallet connectivity. Recent standards like EIP-5792 and EIP-7702 introduced ways to batch multiple calls into one atomic operation via JSON-RPC (e.g. wallet_sendCalls). However, current implementations of those solutions assume an active connection between dApp and wallet (e.g. injected provider or WalletConnect session), which creates an additional layer of UX friction. There is a growing need for a privacy-preserving, frictionless workflow where a dApp or product can trigger complex transactions without “connecting” their wallet or needing to disclosing their address to the application.

Several trends highlight this need:

  • Atomic Multi-Call Transactions: With the advent of account abstraction features like EIP-7702, externally owned accounts (EOAs) can execute batch transactions a la smart contract wallets. Users expect to combine multiple actions (e.g. token approvals, swaps, transfers) into one confirmed transaction for better UX and guaranteed all-or-nothing execution. Developers likewise want to avoid partial failures or multiple prompts.

  • Privacy Concerns: Current wallet connection flows (EIP-1193 / EIP-1102) require dApps to request access to user accounts, linking the user’s address with the dApp even before a transaction is made. By decoupling transaction construction from user identification, we improve privacy. The wallet should not need to announce “who” it is to the dApp just to receive a transaction request. A one-way communication means the dApp never learns the user’s address or other account info, mitigating tracking and profiling risks.

  • One-Way Offline Interaction: In many use cases (desktop-to-mobile workflows, point-of-sale terminals, printed media), it’s desirable to communicate a transaction request via a QR code, NFC tag, or URL without establishing a session. Protocols like WalletConnect provide a session-based two-way link, but are heavyweight when a simple one-off action is needed, and they reveal the user’s account to the dApp. A unidirectional link allows, for example, a user to scan a QR code on a webpage or poster and complete an on-chain action entirely within their wallet app. This also enables fully offline dApps to hand off transactions securely to user devices.

  • Compactness for QR/NFC: Encoding transaction data for use in QR codes or NFC imposes strict size limits. Prior standards (e.g. EIP-681 Ethereum URIs) used human-readable formats that become lengthy when including contract data (hex-encoded addresses and calldata inflate the size). WalletConnect addressed some issues by introducing a more efficient URI scheme (ERC-1328) instead of embedding JSON in QR codes. UWULink builds on this principle by using a concise binary serialization (Protocol Buffers), allowing more data to be communicated in a QR code or NFC tap.

  • Programmability and Offloading Logic: There are scenarios where the exact list of calls depends on on-chain state or user-specific data (for example, an airdrop claim that needs to gather all tokens claimable by that user, or a DeFi interaction where allowances may already exist). In such cases, encoding all call data statically could be unwieldy or inefficient if the dApp lacks knowledge of the user’s address. UWULink’s Programmable Mode allows a dApp to redirect to a deterministic on-chain generator (a smart contract with a standard interface) that can produce the list of calls that the wallet should invoke. This allows the app to invoke arbitrary logic in generating the list of calls based on the state of the blockchain, still without exposing the user’s address.

By addressing these points, UWULink aims to enhance user experience with one-shot multi-call transactions and improve security and privacy by eliminating unnecessary data sharing.

Specification

Overview

UWULink defines a protobuf message format and interpretation for transaction requests sent from a dApp to a wallet. The only operation in scope is a request for the wallet to execute an atomic batch of contract calls on an EVM-compatible blockchain. The wallet, upon receiving a UWULink request (for example, via a QR code scan, deep link, or NFC), will decode it, present the details to the user for confirmation, and if approved, execute the calls as a single transaction on the specified chain.

Key characteristics of the protocol:

  • Unidirectional Communication: Communication is only dApp → wallet. The dApp encodes a request and the wallet handles it. There is no handshake or return channel in the protocol itself. The wallet is not required (or able) to send any data back to the dApp. This one-way design ensures the dApp does not learn any information about the user or wallet (such as the address or which wallet app is used) at request time. It also simplifies implementation – the dApp’s job is simply to generate a batch of calls and display it, and the wallet’s job is to execute or reject the batch.

  • Atomic Batch Calls: All calls listed in the request MUST be executed atomically, i.e. all succeed or all fail together. If any specific call would revert, the wallet should revert the entire batch.

  • No Identity / Auth Required: Because the request is self-contained, the wallet does not need to pre-authorize the dApp or reveal the selected account. In traditional injected scenarios, a dApp would call eth_requestAccounts (as per EIP-1102) to get the user’s address. With UWULink, the first and only interaction is the user willingly importing the transaction request (e.g. scanning the QR). The wallet should treat it similarly to how it would treat a transaction payload from a connected dApp, except no connection context exists. If the request is malformed or not supported, the wallet can simply alert the user and refuse.

  • Binary Encoding: UWULink messages are encoded as a binary blob based on a Protocol Buffers schema. This blob is then further encoded into a URI for transport via QR code or NFC tag. The string encoding of a request is the uwulink: scheme followed by the base 64 encoded message:

    uwulink:<base64_of_UWULink_message>
    
  • Chain Identification: The message includes a chain ID to indicate which chain the calls are intended for. The wallet must verify or use this chain ID when executing the transaction. If the wallet is not currently on the target chain, it SHOULD prompt the user to switch to that chain or automatically switch if permissible (similar to handling of chainId in EIP-681 URIs). If the wallet cannot operate on the requested chain, it must reject the request. This ensures that the dApp’s intent (which chain’s contracts to interact with) is preserved and avoids confusion if the user is on a different network.

  • Static vs Programmable Mode: There are two mutually exclusive ways to specify the batch of calls:

    1. Static Mode: The request directly contains the list of calls (each with target address, calldata, and optionally ETH value) that the wallet should execute in order. This mode is straightforward and similar to existing multi-call APIs (e.g. the JSON-RPC wallet_sendCalls payload from EIP-5792), but encoded in a compact binary form. This is ideal when the dApp knows exactly what actions need to be performed.
    2. Programmable Mode: The request contains a reference to an on-chain “resolver” contract and an input data blob. The wallet will call a predefined view function on that contract (off-chain, via eth_call) to retrieve the actual list of calls to execute. The resolver contract must implement a standardized interface (detailed below) that takes the provided input (and possibly the caller’s address or other context) and returns a set of calls. This mode allows dynamic computation of call lists at execution time. It improves flexibility (the dApp can offload complex logic or personalization to the blockchain) and keeps the QR/NFC payload small since it only carries a contract address and input, rather than every call’s details. For example, a dApp could include just a reference like “resolver contract X with input Y” and that contract’s resolver function will output perhaps dozens of calls based on the latest on-chain state, the user’s address, etc.

The UWULink message includes a oneof/union to indicate which mode is used. Wallets SHOULD support both modes.

Protobuf Schema

Below is the proposed Protocol Buffers v3 schema defining the UWULink message format:

syntax = "proto3";

package org.ethereum.uwulink;

// The top-level UWULink transaction request message.
message UWULinkRequest {
  uint64 chain_id = 1;  // EIP-155 chain ID for the target chain

  oneof request_type {
    Batch batch = 2;
    ResolverReference resolver = 3;
  }
}

// Static batch of calls
message Batch {
  repeated Call calls = 1;
}

// Single contract call
message Call {
  bytes to = 1;                // 20-byte address of target contract
  optional bytes value = 2;    // (optional) up to 32-byte big-endian ETH value
  optional bytes data = 3;     // (optional) calldata for the call
}

// Reference to a resolver contract for dynamic call generation
message ResolverReference {
  bytes resolver_address = 1;  // 20-byte address of resolver contract
  bytes resolver_data = 2;     // opaque data to pass to resolver
}

Notes on the schema:

  • We use bytes for addresses and other binary data. A conforming wallet implementation MUST enforce that Call.to and ResolverReference.resolver_address are exactly 20 bytes. The protobuf itself won’t enforce length, but using a different length should cause the wallet to reject the message (to avoid ambiguity or mis-interpretation). The value field in Call can be 0 bytes (interpreted as 0 ETH) up to 32 bytes. Leading zeros in value SHOULD be stripped in the encoding for consistency (e.g., 1 wei would be encoded as 0x01 not 32 bytes padded; conversely the decoder should treat a missing value or empty value as 0).

  • All fields are numbered for efficient encoding. The oneof request_type ensures only one of Batch or ResolverReference is in use. If an unknown field is present (e.g., a future extension), the wallet should ignore those unknown fields per protobuf default behavior, but core fields must be present for validity (chain_id and one of batch/resolver).

  • If using a text encoding (like Base64) to embed in a URI, the entire UWULinkRequest message is serialized to a binary string, then that binary is base64-encoded. For URI safety, base64 output may need to be URL-encoded (i.e., +, / characters percent-encoded or using the URL-safe base64 variant). This is an implementation detail, but wallet developers should be aware when parsing input. In all cases, the underlying data after decoding is expected to match the protobuf schema above.

  • The schema is chosen for broad compatibility. Proto3’s varint encoding will handle the chain_id (which is usually small like 1, 137, etc.) in 1-2 bytes. The calls repeated field will simply concatenate call entries. Each call entry will have a 1-byte field tag for to followed by 20 bytes address, etc. This results in a very compact representation.

  • Example of an encoded message (for illustration): A static request for chain 1 with two calls might look like:

    • Call 1: to = 0x111111...1111, value = none (0), data = 0xabcdef
    • Call 2: to = 0x222222...2222, value = 100 wei, data = (empty)
    • After encoding in protobuf and base64, the URI could be: ethereum:uwulink?request=EiABAggDEhARERERERERERERERERERERERERERIAGKDCr+8= (this is a fake example string for concept; actual encoding would differ).
    • The wallet would decode that back to the structured fields.

Wallet and dApp developers can import this .proto to ensure they are constructing and parsing UWULink messages consistently.

Resolver Contract Interface (Programmable Mode)

This standard introduces an interface that resolver contracts must implement so that wallets can query them for call batches. All resolver contracts MUST implement the following ABI (interface identifier UWUResolver):

/// @title UWULink Resolver Interface
interface UWUResolver {
    struct Call {
        address target;
        uint256 value;
        bytes data;
    }

    // Thrown when calls could not be generated, with an error code specific to this resolver.
    error CallGenerationFailure(uint256 errorCode);

    /**
     * @notice Compute a batch of calls for a given request.
     * @param requester The address of the wallet (EOA or contract) that is requesting the calls.
     * @param data Arbitrary request data (opaque to the wallet, provided by dApp via UWULink).
     * @return calls The list of calls that correspond to the requester and request
     */
    function getCalls(address requester, bytes calldata data) external returns (Call[] memory calls);

    /**
     * @notice Returns the details for the given error code. Meant to be called by developers to better understand the error code for a resolver.
     *  Due to localization needs, it is expected that developers may call this function, but the wallet should not show this information to users.
     */
    function getErrorCodeDetails(uint256 errorCode) external returns (string memory information);
}
  • The function MAY modify state. Wallets SHOULD call it off-chain, and avoid combining the call with others e.g. via Multicall.
  • The requester is included to allow the contract to tailor results to the specific user. For example, a resolver could check requester’s token holdings or permissions and then return different call sets. The wallet should supply its own sending address as requester. This means that the user’s address is revealed only to the RPC server used by the wallet via this call, not to the dApp server or UI. In the future with the propagation of light clients, it’s possible for the wallet to avoid revealing this information.
  • The data parameter is the exact bytes provided in the UWULink request’s resolver_data. Its contents and encoding are defined by the dApp’s usage and the contract’s logic. For instance, it might contain an enum indicating which action to perform, or some user-specific claim ID, etc.
  • The size of the returned arrays is not explicitly limited by this standard, but practical use should keep it reasonable (dozens rather than thousands of calls) both for blockchain computation reasons and for the user’s ability to comprehend the request.

Wallets should implement the following logic for programmable requests:

  1. Perform an eth_call to resolver_address with to = resolver_address, from = address(0), data = ABIEncodeWithSelector(UWUResolver.getCalls, userAddress, resolver_data) against the latest block. The wallet MAY use the pending block, or otherwise include transactions in the state that are yet to be included in a confirmed block.
  2. If the call returns successfully, decode the result. This becomes the batch of calls to execute. The wallet should then proceed exactly as if it were a static mode request containing those calls. It should display these calls to the user for confirmation (including target addresses, values, and perhaps decoded method signatures if it can).
  3. If the call fails (reverts or is not implemented), the wallet MUST abort. It SHOULD surface an error to the user like “Transaction request generation failed: resolver contract call was unsuccessful.” The user then knows the dApp’s request was bad or the contract might be wrong.

The dApp developer and resolver contract developer are responsible for ensuring that calling getCalls is not too gas-intensive to execute (since wallets will execute it off-chain but it still must complete execution). Excessive computation could result in the node returning an error (out of gas exception in the eth_call context). Typically these functions will just gather data from known contracts or encode some predefined calls, which should not be prohibitively expensive.

Example Usage

To illustrate how UWULink can be used in practice, consider the following scenarios:

1. Static Mode – Token Approval and Swap (DeFi use-case):

Alice wants to trade tokens on a decentralized exchange (DEX) using her mobile wallet, but she doesn’t want to connect her wallet to the DEX website due to privacy concerns. The DEX dApp prepares a UWULink QR code for the trade. When Alice selects the tokens and amount on the website, the dApp formulates two contract calls: one to the ERC-20 token contract to approve() the DEX’s router contract, and one to the router contract to execute the swap ( swapExactTokensForTokens, for example). Normally this would be two separate transactions with two confirmations. Instead, the dApp bundles them:

  • Call #1: to = TokenContract, data = approve(router, amount)
  • Call #2: to = RouterContract, data = swapExactTokensForTokens(params...)

Both calls have value = 0 (no ETH being sent directly). The dApp encodes these into a UWULinkRequest (static mode) for the current chain (e.g. Ethereum mainnet chain_id 1). The protobuf binary is base64 encoded and placed into a QR code with a URI like:

uwulink:CgEBEiAx...   (truncated)

Alice scans this QR with her wallet app. The wallet decodes the request: chain_id=1, two calls in batch. It recognizes it can execute an atomic batch (Alice’s wallet supports EIP-7702). The wallet UI shows Alice a summary: “This dApp is requesting two actions: (1) Approve Token XYZ for spending, (2) Swap Token XYZ for Token ABC on DEX.” Alice can inspect the contract addresses (perhaps the wallet resolves known token/contract names or shows the hex addresses) and the parameters. She sees that both will be submitted together in one transaction. The UI might look similar to a multi-call confirmation screen.

Alice accepts. Her wallet internally either crafts a 0x4 type transaction (since Alice is an EOA on Ethereum) embedding bytecode to do the two calls, or uses its smart wallet module. It then signs and broadcasts the transaction. On-chain, the two calls execute one after the other, and because of atomicity, if the swap were to fail, the approve would be reverted too (avoiding a scenario where she approved tokens without actually swapping).

The DEX backend or frontend can monitor the blockchain for the transaction receipt (it knows what actions it expected, or Alice can manually input the tx hash if needed). The important part is the DEX never learned Alice’s address beforehand; it only sees it when the transaction hits the blockchain, which is unavoidable for executing the trade but at that point privacy is preserved as well as any normal on-chain interaction (the dApp cannot link it to Alice’s web session unless Alice herself tells it out-of-band). This shows how UWULink achieves one-scan confirmation for what used to be multi-step, and keeps Alice’s identity private until the on-chain execution.

2. Programmable Mode – Personalized Airdrop Claim:

A project is running an airdrop where eligible users can claim several different token rewards based on on-chain activity. Bob visits the airdrop dApp page. The page could ask Bob to connect his wallet to figure out what he’s eligible for, but Bob is cautious. Instead, the dApp uses UWULink in programmable mode. It has a resolver contract deployed on-chain which, given a user address, can determine all the reward token contracts and amounts that the user is entitled to claim.

The dApp shows Bob a “Claim Rewards” button, which reveals a QR code. This QR encodes a UWULink request with:

  • chain_id = 5 (Goerli testnet, for example, where the airdrop is happening).
  • resolver_address = 0xDeeD…1234 (the address of the AirdropResolver contract).
  • resolver_data = some bytes encoding maybe an airdrop campaign identifier or simply empty if one global campaign.

Bob scans this with his wallet. The wallet sees it’s a resolver-type request. It calls getBatchCalls(BobAddress, resolver_data) on 0xDeeD...1234 (as a view call). The AirdropResolver contract looks up internally that BobAddress is eligible for 3 tokens: TokenA, TokenB, and TokenC with certain amounts, and the claim function for each is claim(address claimant, uint256 amount) on each token’s distributor contract. It returns three arrays: targets = [AddrA, AddrB, AddrC], values = [0,0,0] (no ETH needed), callData = [ abi.encodeWithSelector(Distributor.claim, Bob, amtA), ... ] for each token.

The wallet receives these arrays. It now has three calls to execute. It shows Bob: “Claim TokenA: amount X, Claim TokenB: amount Y, Claim TokenC: amount Z” (assuming the wallet can decode the function signatures or at least show contract addresses and method names if it has ABIs). Bob approves the batch. The wallet then either directly calls each distributor’s claim in one aggregated transaction. Because Bob’s address was provided to the resolver, each claim call will credit tokens to Bob (likely the contract uses the provided address or msg.sender – here it was likely coded to use the address parameter, since the actual transaction sender will be Bob’s own address in the batch execution context). The important part is Bob did not have to connect his wallet to the dApp; the eligibility and calls were determined by the on-chain contract. The dApp never saw Bob’s address, yet Bob gets his tokens in one go.

After execution, Bob’s wallet shows the transaction success. The dApp might simply tell him to check his balances (or it could have a public page showing which addresses claimed, etc., but it did not get a direct notification — it relies on Bob or the blockchain to know the claim happened).

3. Cross-Device Payment via NFC (Point of Sale):

Carol is at a merchant’s point-of-sale device that accepts cryptocurrency payments via Ethereum. The merchant’s device can display a QR or emit an NFC message with a payment request. Instead of using a simple one-address payment URI (as in EIP-681), the merchant uses UWULink to request a more sophisticated transaction: perhaps Carol will pay through a specific escrow contract or with a certain token if she has a discount coupon.

The device sends an NFC payload which Carol’s phone picks up (many wallet apps can register as handlers for certain NDEF messages or custom URI schemes). The payload contains a UWULinkRequest in static mode:

  • chain_id = 137 (Polygon, where the merchant operates).
  • Two calls: first call to a stablecoin contract’s transfer(merchantAddress, amount) (to pay the merchant), second call to a logging contract registerPurchase(merchantId, CarolAddress, amount) (to log the sale in an on-chain registry). Both calls are value 0 since a token transfer, not ETH, is used.

Carol’s wallet opens with the decoded request: It shows “Pay 50 USDC to Merchant XYZ and register purchase.” Carol sees the merchant name resolved from the merchant’s address (if her wallet has ENS or a local registry of known merchants). She approves. The wallet then executes an atomic transaction on Polygon that calls the USDC token contract and the registry contract. The merchant’s PoS waits for confirmation on-chain (or simply trust the signed transaction once broadcast, depending on their risk tolerance). Carol’s identity remained pseudonymous; the merchant’s device did not directly get her wallet info, it only received the on-chain payment. And Carol only had to tap once to approve both token transfer and logging, rather than scan one QR to pay then perhaps another to log, etc.

These examples demonstrate the flexibility of UWULink:

  • In all cases, the user did not pre-connect their wallet to the application.
  • The requests can be transferred via out-of-band channels (QR/NFC/URL).
  • Multi-step operations become “one-click” (or one-scan) operations for the user.
  • The on-chain outcome is the same as if the user had manually sent those transactions, but with improved UX and privacy.

Rationale

TBD

Backwards Compatibility

UWULink is an additive protocol and does not break any existing standards. It is designed to coexist with current methods:

  • Existing Wallet URIs (EIP-681, EIP-831): UWULink can be seen as an evolution of the idea behind EIP-681 ( transaction request URIs). EIP-681 defines URIs for a single transaction (or payment) and is already supported in a limited number of wallets for QR code scanning. UWULink extends the concept to multiple calls and binary encoding. A wallet that does not recognize the uwulink: scheme should simply not act on it. Typically, such a wallet would either show an error or ignore a scanned QR it cannot parse. This is a graceful failure from the user’s perspective (they’ll know the wallet doesn’t support that request). There is no risk of confusing an UWULink QR with an EIP-681 QR, since the scheme and content format differ. Therefore, wallets that only implement support for EIP-681 will not mistakenly handle a UWULink payload as a valid request.

  • Ensuring Backwards Compatibility in Data Format: The protobuf schema is designed such that new fields could be added in the future in a non-breaking way (per Proto3 rules, unknown fields are ignored by receivers). For example, a future version might add an optional uint64 expiration_timestamp or string origin field to carry a domain name for UI display. An older wallet would ignore these and still execute the core request. This forward-compatibility means UWULink can evolve without breaking older implementations, as long as additions are carefully made optional.

  • Fall-back to Standard Flows: From a dApp perspective, implementing UWULink does not preclude supporting traditional wallet connections. A dApp can offer UWULink QR codes for users who prefer privacy or are on devices (like a separate mobile) without browser extensions. At the same time, it can have the usual “Connect Wallet” button for users who are okay with that. This multi-modal approach ensures no user is left out. Over time, if UWULink (or similar one-way flows) prove safer and more popular, they might become the default, allowing users to interact with dApps without connecting a wallet.

  • Network Compatibility: We limit scope to EVM-compatible chains. That means chains that use EIP-155 transaction scheme and Ethereum-like addresses. On non-EVM chains, this standard doesn’t apply (though analogous concepts could). Within EVM chains, a nuance: if a chain has a different maximum gas limit or transaction format peculiarity, the wallet internally deals with that. UWULink just says “execute these calls.” As long as the wallet can create a transaction that does so, it’s fine. If an EVM chain does not support atomic multi-call (some L2s or sidechains might not immediately support EIP-7702), the wallet has to handle it at the account abstraction layer if possible, or otherwise MUST reject the request. This again falls to the wallet to know its capabilities (e.g. per EIP-5792’s capabilities query).

In conclusion, UWULink aims to introduce new functionality without disrupting existing user journeys. It is opt-in for all parties. Early adopters (both dApps and wallets) can experiment with it while others continue as usual. As support grows, it could become a widely recognized standard for secure one-way wallet interactions. The design takes into account lessons from previous proposals (EIP-681 URIs, WalletConnect, EIP-5792, etc.) and ensures that adopting UWULink is a low-risk enhancement rather than a breaking change to Ethereum’s ecosystem.

Security Considerations

Privacy

UWULink is designed with privacy in mind, but it introduces some new security aspects that implementers and users should consider:

  • No Wallet Identification: The wallet does not disclose the user’s address or any wallet details to the dApp when using UWULink. This significantly improves privacy compared to typical wallet connect flows. The dApp only learns of the user’s address if and when the transaction is broadcast on-chain. Even then, the dApp cannot easily correlate that address with a specific user session (the user could be anonymous on the website until that point).
  • On-Chain Resolver Calls: In programmable mode, the user’s address is supplied to the resolver contract as a parameter. This happens off-chain via eth_call, so it does not create a public transaction. However, the node or RPC provider that the wallet uses will see that call (just like any read call). If the RPC provider is untrusted, this could leak some information (e.g., that this address is interested in this resolver’s data). In most cases this is a minor concern (no more revealing than using the dApp itself while connected to an RPC), but users who are extremely privacy-conscious might prefer static mode or ensure they use a privacy-respecting RPC. Importantly, the dApp backend or frontend does not see this – only the blockchain infrastructure does.
  • No Third-Party Tracking: Because UWULink can be used via local channels (QR/NFC), it avoids relying on any centralized relay. WalletConnect v1, for instance, used relay servers and handshake topics which, in theory, could be tracked or snooped (even though payloads were encrypted, the metadata might leak usage patterns). UWULink in contrast can be a completely peer-to-peer (user and dApp) interaction with minimal digital footprint aside from the eventual blockchain transaction.
  • User Consent: As with any transaction, the user explicitly consents by scanning and approving the request. The user relies on the wallet’s simulation and multi-factor authorization capabilities to prevent sending of malicious transactions.

Security of Transaction Requests

  • Phishing and Malicious QR Codes: A malicious actor could present a user with a UWULink QR code that, if scanned and approved blindly, could cause the user to transfer funds or approve tokens to the attacker. This risk is analogous to phishing links or malicious dApp websites in today’s context. Users should be educated to only approve UWULink requests from sources they trust or understand. Wallets should help by displaying clear human-readable information about what the request will do:

    • Show the names or ENS of known contract addresses involved (or at least highlight unknown addresses).
    • Decode function selectors to known function names if possible (e.g., show “approve(address _spender, uint256 _value)” instead of raw hex).
    • For value transfers, show the ETH or token amount in a friendly format.
    • Possibly warn if the request involves calling an unrecognized contract with large value transfers or if it sets a high token allowance, etc.
  • Atomic Execution and Reverts: By enforcing atomic execution, UWULink ensures that partial completion won’t lead to stuck funds or unintended states. However, this also means a malicious or buggy request could be crafted to always revert (for example, by including an incompatible call), which could waste user gas fees if not caught. Wallets should simulate the batch when possible. If the wallet can do a dry-run (for instance using eth_call on a Bundler or internal simulation) it might detect a guaranteed revert and inform the user that the call set is invalid (though this might be complex to do reliably for all calls).

  • Resolver Contract Trust: The programmable mode introduces a potential trust issue: the user is effectively trusting the resolver contract’s code to generate the calls honestly. If the resolver contract is malicious, it could return call data that benefits an attacker. For example, a malicious resolver could ignore the input data and always return a call transferring all of the user’s ETH to the attacker’s address. Mitigations:

    • Ideally, resolver contracts should be open source and verified, and the dApp using them should be reputable. The wallet can’t fully know if the resolver’s output is malicious until it sees it, but the user will have a chance to review the resulting calls anyway. This is crucial: the wallet must display the resulting calls from the resolver to the user, just as it would in static mode. The user should then notice if something is off (e.g., a transfer of all their ETH is about to happen).
    • Wallet developers might consider adding special handling or warnings if a resolver returns calls that do not seem correlated with the input. However, this is hard to generalize. At minimum, treat the resolver output with the same suspicion as a static request. There’s no inherent additional risk beyond what static mode has, because the user still confirms the final calls. The difference is just where the call data came from.
    • We assume resolver contracts will often be provided by the same party as the dApp and thus come with an implied level of trust (or at least, they can be audited by the community if the UWULink scheme becomes popular).
  • No Automatic Spending: UWULink does not introduce new signing or authorization paradigms – it uses actual transactions that the user signs on the spot. Thus, it’s less prone to the kind of issues where a signature can be later reused (like the risks with off-chain signatures). Each UWULink request is one transaction (with possibly multiple subcalls). After it’s executed, the link cannot be reused to automatically trigger more actions (unless the user scans it again). This is good from a security standpoint since it doesn’t create long-lived permissions. One exception: if a call within the batch is an approval or something, that is an on-chain permission that persists as usual (the user should be made aware as normal).
  • Denial of Service (DOS): A malicious dApp could craft an extremely large UWULink payload (especially in static mode) that could crash or slow a wallet app upon scanning (due to memory or decoding issues). Wallets should implement size limits and perhaps streaming parsing for the protobuf to avoid crashes. If a payload exceeds a reasonable size (e.g., several kilobytes), the wallet can reject it for safety. Similarly, a resolver contract could try to return extremely large results – wallets should guard against that by limiting the amount of gas provided to the resolver via eth_call.
  • Capabilities and Future Extensions: UWULink intentionally does not carry any additional flags like gas limits or paymaster info (unlike EIP-5792 which has a capabilities system). This is to keep the format simple. However, this means the wallet will apply its own heuristics for gas, and by default the user pays fees. If a future extension wanted to allow gas sponsorship or other features, that could be added either by extending the protobuf (e.g., adding an optional paymaster field) or by having the resolver contract itself handle that (e.g., a resolver could incorporate a paymaster logic by returning a call to a paymaster contract as part of the batch). In any case, security considerations around gas (like a malicious paymaster causing some weird behavior) would need to be analyzed. For now, UWULink operates within the normal transaction model, so the main security focus is on the correctness of calls.

Comparison to Traditional Flows: One might ask, does eliminating the wallet <-> dApp handshake create any new risks? In traditional connected dApp sessions, the wallet at least knows the origin of requests (e.g., which website is calling eth_sendTransaction or wallet_sendCalls). In UWULink, the origin is essentially “the QR code the user scanned” – the wallet might know the payload came via a QR/NFC but not which app or site. In security terms, this means the wallet cannot apply domain-based whitelists or blocklists (since there’s no domain, unless the URI contains one in the payload which it typically wouldn’t). Therefore, the user must manually trust and verify each request. This is akin to using a hardware wallet: every transaction is shown on a screen and the user approves it, with no assumptions about where it came from. This places responsibility on the user and makes the wallet’s job of displaying info accurately even more important.

Privacy vs. Usability Trade-off: Because UWULink doesn’t let the dApp query the wallet off-chain, some conveniences are lost – e.g., the dApp cannot automatically fetch the user’s address to display their balance or NFTs in the UI prior to a transaction. This is a conscious privacy trade-off. Some advanced dApps might find workarounds (like asking the user to input their address manually if they want to see personalized info, or shifting more logic on-chain as in programmable mode). Users and dApp developers must understand this trade-off. In contexts where user personalization without login is needed, UWULink might require a bit more creativity, but it ensures that if the user chooses not to share anything, they truly don’t until a transaction is made.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Moody Salem (@moodysalem), Tina Zheng (@tinaszheng), "ERC-7946: Unidirectional Wallet Uplink aka UWULink [DRAFT]," Ethereum Improvement Proposals, no. 7946, May 2025. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7946.