diff --git a/ERCS/erc-7946.md b/ERCS/erc-7946.md new file mode 100644 index 00000000000..9d8f56adb50 --- /dev/null +++ b/ERCS/erc-7946.md @@ -0,0 +1,549 @@ +--- +eip: 7946 +title: Unidirectional Wallet Uplink aka UWULink +description: Unidirectional app to wallet communication for private construction of transactions +author: Moody Salem (@moodysalem), Tina Zheng (@tinaszheng) +discussions-to: https://ethereum-magicians.org/t/erc-7946-unidirectional-wallet-uplink-aka-uwulink/24282 +status: Draft +type: Standards Track +category: ERC +created: 2025-05-10 +requires: 155, 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: + ``` + +- **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: + +```protobuf +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`): + +```solidity +/// @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 + +Copyright and related rights waived via [CC0](../LICENSE.md). + +[EIP-155]: ./eip-155.md +[EIP-5792]: ./eip-5792.md +[EIP-7702]: ./eip-7702.md +[EIP-1193]: ./eip-1193.md +[EIP-1102]: ./eip-1102.md +[EIP-681]: ./eip-681.md +[ERC-1328]: ./eip-1328.md +[EIP-5792]: ./eip-5792.md +[EIP-831]: ./eip-831.md +[ERC-20]: ./eip-20.md