Blind signing: what you are actually approving when the screen says nothing useful

·6 min read·By SSP Editorial Team
SSP Academy cover: blind signing and unreadable transaction approvals

Blind signing: what you are actually approving when the screen says nothing useful

There is a specific moment where most large crypto losses happen. It is not when a key is stolen. It is when someone looks at a signing prompt they don't understand, decides it is probably fine, and taps approve.

The industry has a name for this: blind signing. It means putting your signature on data you cannot read. And for years, wallets treated it as normal.

What a signing screen is supposed to do

The security promise of self-custody rests on one assumption: that nothing moves without your approval. Two-of-two multisig, hardware wallets, air-gapped signers — every one of them is a machine for turning your intent into a signature.

That machinery is worth exactly as much as your understanding of what you approved.

If the screen says "Send 0.5 ETH to 0x8f3C…", your approval means something. You compared it against what you meant to do. If the screen says "Contract interaction — data: 0xa22cb465000000000000000000000000d8dA6…0001", your approval means nothing at all. You are not consenting to a transaction; you are consenting to a rectangle of hexadecimal.

Attackers know which of those two screens they need you to see.

What hides in the bytes

An EVM transaction's payload is calldata: a four-byte function selector followed by ABI-encoded arguments. It is perfectly readable by a machine and perfectly opaque to a person. A handful of those selectors are where the money goes.

approve(address,uint256) — selector 095ea7b3. Grants a contract permission to spend your tokens. Not a transfer; a standing authorisation. It moves nothing when you sign it, which is exactly why it slips through. The drain happens later, on the attacker's schedule.

setApprovalForAll(address,bool) — selector a22cb465. The NFT equivalent, and worse: one signature grants an operator authority over every token in that collection you hold, now and in the future. There is no amount field to be reassured by.

increaseAllowance(address,uint256) — selector 39509351. Tops up an existing allowance. Often overlooked because it isn't the selector everyone was told to watch for.

transferFrom(address,address,uint256) — selector 23b872dd. Moves tokens from an address that already granted an allowance. The signature that finally spends what an earlier approve authorised.

The pattern worth internalising: the transaction that steals your money is rarely the transaction you signed. You signed permission. The theft is a separate transaction, sent later, that you never see.

Why "unlimited" is the word to look for

Almost every allowance exploit shares one feature: the amount is effectively infinite.

Dapps ask for unlimited approvals because it is convenient — approve once, never prompt again. The result is that users are trained to grant unbounded spending rights routinely, and an interface that shows 115792089237316195423570985008687907853269984665640564039457584007913129639935 is not telling anyone anything.

There is a subtlety that catches naive implementations. The canonical "infinite approval" value is exactly 2²⁵⁶−1, so the obvious check is to compare against that constant. But dapps routinely use other astronomically large numbers — half of max, 0xff…f0, 10¹⁸ × 10³⁸ — that are unlimited in every practical sense while failing an exact-match test. A warning that only fires on the exact sentinel is a warning an attacker can step around by subtracting one.

The right threshold is well below the sentinel and well above anything real. SSP flags any allowance at or above 2²⁵⁵ — roughly 5.8 × 10⁷⁶, which exceeds any possible ERC-20 supply by dozens of orders of magnitude. Nothing legitimate is ever misflagged, and grinding a value just under the maximum doesn't dodge the warning.

That threshold applies only to allowance-granting calls. On a plain transfer, "unlimited" isn't a meaningful idea — you're moving a specific amount — so the exact-max sentinel is left alone there.

What SSP does

SSP decodes calldata into plain language on the approval screen. Recognised selectors are rendered as what they actually are: who the counterparty is, what the amount is, whether the rights being granted are unbounded. Raw hex is no longer the primary content — it lives behind an Advanced section for people who want it.

Three design decisions matter more than the decoding itself.

The decoder is presentation-only. It never changes what gets signed. The approval always signs the exact original payload; the helper only re-presents bytes that were previously shown as raw hex. A decoder that could alter the payload would be a new attack surface rather than a defence — the thing you read and the thing you sign must be the same bytes, always.

It fails closed. Unknown selector, wrong length, malformed hex, non-standard address padding — anything unexpected returns nothing, and the UI falls back to a generic action with raw hex in Advanced. It never guesses. A decoder that guesses is worse than no decoder, because a confident wrong summary is more dangerous than visible hex: hex at least tells you honestly that you don't understand it.

That fail-closed instinct runs deeper than unknown functions. An ABI-encoded address is twelve zero bytes followed by twenty address bytes; a word with anything else in those leading bytes is not canonically encoded, and SSP treats it as suspicious rather than trying to interpret it. Same for booleans — only the canonical all-zero (false) and 31-zeros-then-0x01 (true) encodings are accepted. Non-canonical encodings on a screen that grants spending rights are a red flag, not a parsing challenge.

Token symbols and decimals are never guessed. A human amount is only shown when the token is confidently known from the on-device registry. Otherwise you get the raw base-unit number. This is deliberately less pretty: displaying "5.0 USDC" for a contract that merely calls itself USDC would turn the decoder into a lying machine, which is precisely the outcome an attacker wants.

Then there is the part that is structural rather than cosmetic. In SSP the transaction is built in one place and approved in another — composed in the browser extension, decoded and displayed independently on your phone, where SSP Key recomputes the transaction hash on-device and refuses to sign if it doesn't match what was shown. Blind signing on a single device means one compromised screen is enough. Here, the screen that shows you the decoded action and the device that holds the second key are the same device, and it verifies rather than trusts.

What to do with your own approvals

Treat approve and setApprovalForAll as the dangerous ones. They are the signatures that cost people money, and they feel harmless precisely because nothing moves.

Refuse unlimited approvals when you can. Many dapps accept a bounded amount if you set one. It's friction, and it caps your downside at what you actually intended to spend.

Audit what you have already granted. Old approvals do not expire. A permission you gave a protocol two years ago is still live, and if that contract is later compromised, it is a live path to your tokens. Revoking approvals is quick, and it is the highest-value hour of housekeeping in self-custody.

When the screen tells you nothing, that is the signal. If your wallet cannot say what a transaction does, that is information — not an inconvenience to click through. The right response to an unreadable prompt is to stop, not to squint.

The goal was never to make you read hex. It is to make sure that when you approve something, you and your wallet agree on what it was.

Share this article

Related articles