Crypto OS
Technical Crypto OSStage 3 · Full-stack Onchain Application

T13 · Wallet Connection

What actually happens when you sign in to a DApp?

Capabilities practiced
Builder
Hands on
Implement signature-based login and an authorization-revocation entry point; run both flows with a test wallet.
AI Lab
Have AI list common forms of signature phishing. Check whether your interface shows users the actual content they sign.

A Real-World Problem

The product team says: “Add wallet login. It is basically like signing in with WeChat.”

Open any DApp: click “Connect Wallet” in the corner, approve a pop-up, and your avatar appears. It does look similar.

But this flow contains at least three entirely different actions:

  • A browser extension gives the page an address. Nothing about the address has been verified; the page has merely been told one.
  • The page requests a signature and the user approves. What does that prove, and to whom?
  • Some sites immediately show another “Approval” pop-up after login. The user thinks it is still part of signing in and approves that too. This may grant access to every unit of a token in their wallet.

All three look alike: a pop-up and a confirmation click. Their consequences differ completely. The first changes nothing. The worst case for the second is account impersonation. The worst case for the third is losing funds.

Users cannot tell them apart partly because many products have not separated them either. This chapter does exactly that.

Thought Experiment

Imagine one key serving three purposes.

You join a company and receive a badge at reception. It is simultaneously:

  1. An access card: swipe it and a gate opens. It proves only “you are an employee.”
  2. An official signature stamp: press it onto a document to show “I saw and approved this.”
  3. A pen that signs blank checks: once signed, finance must pay, and the signature can be redeemed repeatedly until you revoke it.

These would never be combined in ordinary life. A lost access card can be replaced, a lost stamp requires formal revocation, and no one would hand you a book of blank checks.

A Crypto wallet private key combines all three. The same key opens the gate, signs documents, and signs a repeatable check. The difference is not which key was used but what was written on the paper you signed.

That repeatable “check” has onchain state. Once granted, it remains valid until you send a transaction revoking it. Closing a tab, clearing cache, or changing computers does not remove it. Ordinary Web2 login has no equivalent.

So can users understand the paper they are signing?

Your Decision

You are adding login to your DApp. Which of four approaches do you choose?

What Happens

The four options reveal that connection, authentication, and authorization are separate actions that happen to share a pop-up interface.

ConnectionAuthenticationAuthorization
What the user doesAllows the page to see an addressSigns a messageSigns a transaction
Onchain?NoNoYes, with Gas cost
Who verifies it?No oneYour backendAn onchain contract
What is proved?NothingControl of the private keyPermission to spend funds
Expiry?Ends with page connectionYou define itNone until actively revoked
Cost of failureExposure of a public addressAccount impersonationFunds moved away

Two cells deserve special attention. “No one verifies it” is easy to skip: a connected address can be claimed by anyone. It is suitable for display, not authentication. “No expiry” is the one state that survives closing the page: your product therefore needs a revocation path rather than sending users to find one elsewhere.

The table also omits one warning: “offchain” does not mean “risk-free.” An offchain signature can itself authorize spending. The worst phishing examples below require no transaction from the user.

Build a Model

Separate login into five stages, each with one job:

  1. Connect: obtain address
  2. Authenticate: prove it with a signature
  3. Session: issue a credential
  4. Authorize: approve spending only when needed
  5. Revoke: withdraw approval at any time
The first three establish identity; the last two manage permission. Trigger, present, and explain them separately.

What You Sign Determines Everything

Signature requests fall into three classes with dramatically different risks:

TypeWhat the wallet showsRisk
Human-readable messageComplete understandable textLow, if nothing deceptive is hidden in it
Structured dataField names and valuesMedium; field names can mislead
Raw hashHexadecimal charactersVery high; users cannot tell what they sign

The third is blind signing. A user sees meaningless characters and approves something with unknown consequences. If your product requires blind signing, its design is flawed.

The second deserves even more care because it appears safe. A field named spender holds an address; a field named value holds a 78-digit number. Users can “see” each field without understanding that they are giving a stranger access to their entire balance for a year.

Honest display means users can describe the consequence after reading, not merely that raw data is visible. Those are very different standards.

What a Login Message Should Contain

Here is plain text; the meaning of the fields matters more than the exact format:

example.com wants you to sign in with this address:

0x<your address>

By signing, you agree to sign in to example.com.
This signature does not send a transaction or approve any transfer of funds.

Chain ID: <chain ID>
Nonce: <one-time random value generated by the backend>
Issued At: <issuance time>
Expiration Time: <expiry time>

Each field blocks a class of attack: domain prevents cross-site replay; address prevents impersonation with another account’s message; Chain ID prevents cross-chain replay; Nonce prevents replay on the same site (invalidate it immediately after use); and expiry limits how long a stolen signature works.

“This signature does not send a transaction or approve funds” is a promise to the user, not boilerplate. Each time that proves true, users gain confidence in the message. Slip one approval into login and you spend that trust across the industry.

Session Lifetime

After signature verification, issue an ordinary session credential as you would in Web2. Three Crypto-specific events must invalidate it:

type WalletSession = {
  address: string;      // Verified address
  chainId: number;      // Chain used during authentication
  issuedAt: number;
  expiresAt: number;
};

// Three situations requiring immediate session destruction:
// 1. Wallet switches account, so current address differs from session.address
// 2. Wallet switches chain and a single-chain product sees a chainId mismatch
// 3. Wallet disconnects

The first is easiest to miss. A user changes wallet accounts, but the page retains the old session. They see account B in the wallet while acting on account A’s data in the app. Listen for account changes and destroy the session immediately, requiring new authentication rather than switching silently.

Approvals Must Be Timely, Limited, and Revocable

A spending approval is an onchain transaction. Design it around three questions:

  1. When to ask? When the user actually needs the feature, not during login.
  2. How much? The amount needed for this operation, not unlimited funds. Avoiding one extra click is not worth a permanent exposure.
  3. How to revoke? Your product needs a page listing approvals users have granted to your contracts, with a direct way to revoke them.

The third is half of this chapter’s lab. Teams often say users can go elsewhere to revoke, but a product that creates a risk should provide its exit.

What It Is Called

Wallet AdapterWallet adapter

A layer presenting one interface over different wallet connections: browser extensions, mobile deep links, QR pairing, and smart contract accounts all expose “request account,” “request signature,” and “send transaction.”

Its value is not merely less code. Business logic should not depend on one wallet implementation. Changing wallets should not change the state machine.

SIWESign-In with Ethereum

A convention standardizing login-message fields and order: domain, address, chain ID, nonce, issuance time, and expiry.

Its contribution is not just formatting; wallets can recognize a login message and render it honestly. Inventing your own format forces wallets to show plain text without the same recognition cues.

SessionSession

A credential issued after authentication, meaning “this address proved its identity for this period.”

It is an ordinary web session. Its special aspect is when it must end: account switch, relevant chain switch, or wallet disconnection.

ApproveSpending approval

An onchain transaction allowing an address to spend a specified amount of your asset.

Remember: it persists after closing the page, it can be used repeatedly until exhausted or revoked, and it is unrelated to login.

RevokeRevocation

Sets a previously granted allowance to zero. This too is an onchain transaction that costs Gas.

Revocation blocks future spending but cannot retrieve funds already transferred. Revoke remaining permissions promptly after a phishing incident, but funds already gone do not return.

Blind SigningBlind signing

A wallet shows only unintelligible data, leaving users unable to assess the result of signing.

Blind signing is not a user failure. The system upstream failed to turn data into understandable language. Requiring it asks users to trust you, despite Crypto’s aim of reducing such trust.

Hands-on

Hands onBuild signature-based login and an approval-revocation controlAny frontend framework + a backend + a test walletFree; use testnet throughout

Use only testnet and a wallet holding test tokens, never an address with real assets. Revoking an approval sends a real transaction, so do it on testnet.

Connect and acknowledge that connection proves nothing.

Request an account and get its address and chain ID. Store them in a state named connection, not user.

The name matters: if you call it user, someone will eventually send it to the backend as proof of identity.

Request a nonce from the backend.

Have the backend generate a one-use random value, bind it to the address, store it, and give it a short lifetime (a few minutes is enough).

The backend must generate the nonce. A frontend-generated value cannot prevent replay because attackers can generate one too.

Build the login message and request a signature.

Use the template above, filling in domain, address, chain ID, nonce, issued time, and expiry.

Inspect it in the wallet yourself before relying on it: the displayed text should match what you built, without truncation, garbling, or conversion into a raw hash. If you cannot read it, users cannot either.

Verify the signature on the backend, then consume the nonce.

The backend must do all five:

  1. Recover the address from the signature and compare it with the address in the message
  2. Check that the domain is your domain
  3. Check that the nonce exists and has not been used, then delete it immediately
  4. Check that the message has not expired
  5. Check that the chain ID is expected

Issue a session cookie with HttpOnly, Secure, and SameSite settings after success.

Consume the nonce atomically; otherwise, concurrent requests may both pass.

Implement invalidation.

Listen for account and chain changes. As soon as the address changes, clear frontend state and call backend logout.

Test it yourself: log in, switch wallet accounts, and see whether the page logs out immediately. If it still shows the old address’s data, the bug is in your code now.

Build an approval panel.

Query allowance and list approvals the current user has granted to your contract. Translate amounts into plain language: show “unlimited allowance” prominently for a huge number instead of a long integer.

Add a revoke button that sends a transaction setting the allowance to zero, waits for confirmation, and refreshes the list.

Run two passes and one attack attempt.

First: sign in normally, approve a limited amount, revoke it, and verify zero allowance.

Second: submit the captured signature from the first pass to the backend under a different domain. It must be rejected. Submit the same signature twice to the same backend; the second use must also be rejected. These check the domain and one-use nonce.

After these steps, you have a login flow suitable for production use, and you have verified two of its main defenses yourself.

AI Lab

AI LabAsk AI to list signature-phishing patterns, then check your interface against eachLevel 2 · AI Copilot

Ask in two steps; the second matters most:

Step one:
List common Crypto wallet signature-phishing patterns. For each, explain:
- What type of data the attacker asks users to sign
- What the wallet pop-up actually shows users
- What an attacker can do afterward and whether users must take another action
- Which clues a user could realistically recognize

Step two:
Here is the copy and pop-up content of my login and approval interface (paste the real text).
For every pattern above, check whether my interface communicates the consequence honestly
or could mislead a user. Suggest a specific change for each weak point, not generic advice.

The model will likely enumerate patterns well. The second step is the useful one: use its list as a checklist and force a specific answer to “what does my interface display here?” for each item.

Any item you cannot answer is a risk your product is creating.

Watch for a common bias: the model may focus on fake websites and domains but give too little attention to misleading signatures on a real site. The latter is harder to detect: the URL and site are authentic, but the pop-up authorizes something different from the page’s description. Ask it to analyze that case explicitly.

Verify these points yourself after using AI

  • For every pattern, you can describe exactly what users see in the wallet, not just a general risk
  • Did it include attacks that need only an offchain signature and no user transaction? These are dangerous and easy to omit
  • You tried its proposed data formats and field names in a real wallet to confirm the display
  • Did it confuse a login signature with an authorization signature?
  • Did it offer a mere warning sentence that does not improve how consequences are displayed?
  • For each pattern, you can point to exactly what your own interface displays at that moment

Real-World Case

Unlimited approval requested at login

Many products ask for unlimited approval on first entry to “save a click.”

The user has not used the feature and no money has moved, so it may seem harmless. But from that point, a future flaw in the approved contract exposes all of that token, even if the user never uses the feature again. Repeated incidents have affected addresses that stopped using a protocol years earlier but never revoked approval.

Ask for approval when the feature is used, and request only the needed amount.

Signature phishing that requires no user transaction

Some authorizations can be completed with one offchain signature, with no transaction or Gas paid by the user.

That is dangerous because users learn “signatures are safe; transactions are risky.” “This is only a free signature” lowers caution. An attacker can submit the signature onchain later and move assets. Victims often say, “I did not approve any transaction.”

Offchain and risk-free are different. The interface must explain what the signature permits.

The signature was a sale order, not login

NFT marketplaces often use offline orders: the user signs structured data saying they will sell an asset at a particular price.

An attacker disguises that data within a login-looking flow. The user sees field names, cannot interpret them, and confirms, unknowingly signing a sale order at zero price.

Structured-data fields can have misleading names. Readable does not mean understandable. Honest display means the user can describe the consequence.

A hash on a hardware wallet screen

Some operations show only a hexadecimal string on a hardware wallet’s small display. Users cannot know what they are signing and must trust their computer screen, the very device that may be compromised. A hardware wallet prevents private-key export; it cannot prevent a user from signing something they do not understand.

When designing a protocol, clear display is a requirement, not a decorative extra.

Change One Variable

What ifThe user changes wallet accounts midway

Without an account-change listener, the page enters a dangerous state: it displays account A’s data while the wallet signs as account B. The user believes they are acting as A but actually signs as B, causing wrong data at best and misdirected funds at worst.

There is one sound response: when the address changes, destroy the session and return to the logged-out state. Do not silently switch identities without new authentication.

What ifThe user has a smart contract wallet

A contract wallet does not use a conventional private-key signature in the same way. Its signature is defined and verified by the contract. Your “recover an address from the signature and compare” logic fails.

Verification must use the contract’s interface: give it the message hash and signature, and ask whether the signature is valid for that wallet.

There is a deeper consequence: the controller of a contract wallet can change. Someone who could authorize today may not control it tomorrow. Keep sessions short and reverify important actions. T27 develops account abstraction.

What ifThe login signature has no expiry

If a signature leaks through logs, caching, or interception, it becomes a permanent pass. This can be harder to notice than password theft: users cannot change an old signature and may receive no unusual-login alert.

Expiry does not prevent leakage. It limits the window for harm after leakage. That is the distinction between prevention and limiting blast radius.

What ifYour backend trusts an address supplied by the frontend

Then authentication effectively does not exist. Anyone can send any address to the API and read private data or initiate actions as that address. The mistake is common because the frontend genuinely receives an address, so passing it to the backend feels natural.

A code-review rule: every protected backend endpoint must get identity from the session, not request arguments. Where a request includes an address, ask whether it selects an object to query or claims an identity.

Takeaway Questions

4
Who is the user?

Who is the user? Here that has a precise technical meaning: why does your backend believe this request comes from the address’s controller? If the answer is “the frontend told me,” you do not know who the user is.

9
Who bears the risk?

Who bears the risk? Once approval is granted, ongoing risk falls on the user, often without their noticing. Saving one pop-up in your flow trades away a lasting exposure on their behalf.

18
Which decisions must keep a human in the loop?

Which decisions require a human in the loop? Signing is the simplest example in this course. It often fails not because users refuse to read, but because the pop-up is impossible to understand. Making it understandable is a product responsibility. T32 extends this idea to Agent approvals.

Chapter Check

Key idea

A signature proves identity, not authorization; authorization must be designed separately and be revocable.

Finished this chapter’s lab? Mark it completeView all progress

On this page