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:
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.
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";packageorg.ethereum.uwulink;// The top-level UWULink transaction request message.messageUWULinkRequest{uint64chain_id=1;// EIP-155 chain ID for the target chainoneofrequest_type{Batchbatch=2;ResolverReferenceresolver=3;}}// Static batch of callsmessageBatch{repeatedCallcalls=1;}// Single contract callmessageCall{bytesto=1;// 20-byte address of target contractoptionalbytesvalue=2;// (optional) up to 32-byte big-endian ETH valueoptionalbytesdata=3;// (optional) calldata for the call}// Reference to a resolver contract for dynamic call generationmessageResolverReference{bytesresolver_address=1;// 20-byte address of resolver contractbytesresolver_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
interfaceUWUResolver{structCall{addresstarget;uint256value;bytesdata;}// Thrown when calls could not be generated, with an error code specific to this resolver.
errorCallGenerationFailure(uint256errorCode);/**
* @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
*/functiongetCalls(addressrequester,bytescalldatadata)externalreturns(Call[]memorycalls);/**
* @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.
*/functiongetErrorCodeDetails(uint256errorCode)externalreturns(stringmemoryinformation);}
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:
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.
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).
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.
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.