How Hasp works

Everything the page does, in the order it does it, and everything it is built on.

What Hasp does

An approval is a permission recorded on the chain: "this contract may move this much of this token out of my wallet". Apps ask for them so they can trade or lend for you, and most ask for the largest number a token can hold, so they never need to ask again. Hasp reads every approval an address has given on Robinhood Chain, works out which are still open and what each could take, and sends the calls that set the ones you tick back to zero — from your own wallet, directly to the contract that holds the approval.

Hasp deploys no contract and asks for no approval. There is nothing of Hasp's between you and the token.

Finding approvals

Four kinds, each announced by an event the chain keeps:

  • Token approvals — ERC-20 Approval(owner, spender, value). Read back with allowance(owner, spender).
  • Permit2 allowances — Permit2's Approval and Permit events. Read back with allowance(owner, token, spender), which also gives an expiry. An expired allowance counts as shut.
  • "All my NFTs" approvalsApprovalForAll(owner, operator, approved). Read back with isApprovedForAll.
  • Morpho authorisations — Morpho Blue's SetAuthorization, where the wallet is the second indexed argument. Read back with isAuthorized. An authorised address can borrow and withdraw on your behalf.

The page asks Robinhood Chain's own node for every such event whose owner is the address, from the first block, in ranges of 8,000,000 blocks. The node refuses a range that matches too many events ("exceeds limit") or takes too long ("log query timed out"); both mean the same thing, and the page halves the range until the node answers. It never asks the chain's public mirror for history, because that mirror answers an old query with an empty list — which would read as "nothing to worry about".

History only says which pairs were ever approved. Every number on the results page comes from a second pass: all the current allowances, balances and authorisations, read through Multicall3 at one block, so they describe the same moment.

Pricing them

What an approval could take is the smaller of what it is allowed and what you hold. For Permit2 it is also capped by what the token lets Permit2 itself move. Tokenized stocks are priced at what $100 of USDG buys through their best Uniswap pool at block 64,835,684 (fee included), for 51 stocks; USDG is a dollar and ETH is $2,404.20. A token Hasp has no price for is shown as unpriced, never as zero. "All my NFTs" and Morpho authorisations reach everything of their kind, so they are shown as all of it rather than a number.

Ranking them

This is a judgement, and it is stated here so you can disagree with it. Riskiest first:

  1. A plain wallet — an address with no code. Contracts ask for approvals; a person almost never should. (A wallet that has delegated to code under EIP-7702 is still counted as a wallet.)
  2. Unpublished code — a contract whose source is not published on the explorer.
  3. Published code — a contract with source on the explorer that Hasp does not recognise.
  4. Recognised — Uniswap and Morpho's own contracts, listed below by address.

Within each, the largest dollar figure comes first. The page ticks plain wallets, unpublished code, "all my NFTs" and Morpho authorisations to anything unrecognised, and anything else that could take more than $1.00 — except recognised contracts, which you can still tick yourself. None of this says an approval is malicious.

Shutting them

Four calls, and js/shut.js can build nothing else:

token.approve(spender, 0)
nft.setApprovalForAll(operator, false)
Permit2.lockdown([(token, spender), …])     // every Permit2 pair you ticked, one call
Morpho.setAuthorization(address, false)

Before your wallet is asked, the page decodes what it built and refuses it if any call goes to the wrong contract, sets anything but zero, turns something on, carries ether, touches a pair you did not tick, or misses one you did. Then every call is run against the chain from your address. If your wallet supports atomic batches (EIP-5792), the calls go as one batch you confirm once; otherwise your wallet asks once per call. Afterwards the page reads the chain again and shows what it now says.

Only the wallet that gave an approval can take it back. Anyone can look at any address.

What it does not find

  • Approvals for one NFT. They are cleared when the NFT moves, and Hasp does not list them. The results say how many it skipped.
  • Tokens that change an allowance without announcing it. The ERC-20 standard requires the event; a token that breaks the standard is invisible to any history-based scanner.
  • Bots. An address with more than 20,000 approval events is refused rather than read for many minutes.
  • Other chains. Only Robinhood Chain (chain ID 4663).
  • Signatures you have not used. A Permit2 or EIP-2612 signature you signed but nobody has submitted is not on the chain. Hasp cannot see it; Permit2's invalidateNonces can cancel one, and Hasp does not build that call.

The census

We took the first page of Blockscout's holder list for each of the 51 tokenized stocks Hasp prices — 775 distinct addresses that are not contracts — and ordered them by the hash of their address, which nobody chose, then read the first 60 with the same code the app runs. None of them was a bot. Every current number was read at one block, 64,896,490 (2026-09-16).

Of the 60 wallets, 43 carry at least one open approval and 42 carry an unlimited one. Reading one wallet's whole history took a median of 34 history queries and 49 seconds from this machine. By who holds them: a plain wallet 10 approvals in 1 wallets, reaching $0.00; unpublished code 3,141 approvals in 11 wallets, reaching $12,921; published code 1,016 approvals in 27 wallets, reaching $648,923; recognised 1,632 approvals in 35 wallets, reaching $1.48m — reach overlaps between classes, so these are not additive. Summing every approval's own figure gives $5.34m, which counts a balance once for every contract that can reach it; the capped total is $1.53m.

How it was checked

Nobody has audited Hasp. These are the checks we ran instead, each of which has a control that must fail.

  • Properties on the real chain: 20/20 at block 64,905,282 (tools/props.mjs). Each is one eth_call with no to: contracts/HaspProps.sol makes a stand-in wallet and two stand-in thieves, and runs the bytes js/shut.js built against the real stock token, USDG, Permit2, Uniswap's position NFT and Morpho Blue. Every refusal is classified by what the contract said, and every one has its control in the same call — the same pull going through before the shut. Seven are checks on the calls themselves, five of them controls that must be refused, and one that looks for every selector in the bytecode behind each contract's proxy.
  • Sabotage: 13/13 (tools/sabotage.mjs). The page's builder was broken 13 ways — an approval set to one instead of zero, a Permit2 pair left out, a lockdown sent to the wrong contract, an approval turned on instead of off — and each break had to be caught by the property named for it, after a control run that was entirely green. 3 leave the page's own inspector switched on and are caught by it refusing to build the calls at all.
  • Real wallets: 43/43 (tools/test-page.mjs). For every census wallet with something open, a small executor was installed as the code at the wallet's OWN address by a state override, so the page's calls ran as that wallet against its real approvals: 5,798 approvals in 5,304 calls, sent in 159 batches of up to 40 (one wallet alone had 926 open), all read back shut by the page's own reader. Two controls per wallet: with no calls everything reads open, and the same calls from a stranger's address shut nothing.
  • Fuzz: 4/4 seeds clean, 13/13 breaks caught (tools/fuzz.mjs). 4 seeded sequences of 80 random operations each — approvals, pulls, Permit2 allowances that expire, stock arriving and leaving, NFT operators, Morpho authorisations, and shuts of a random subset — plus a coda that forces every kind, on a fork of Robinhood Chain at block 64895966, against the real stock token, USDG, Permit2, Morpho Blue and Uniswap's position NFT. After every step the chain is read directly and compared with a model of what the harness did, and the page's own reader must agree with both. Then 13 deliberately broken copies of the reader and builder were replayed on the same seeds; each had to be caught by the invariant named for it.
  • The page in a browser: 26/26 (tools/e2e.mjs). The real app in a real browser, served with the deployed Content-Security-Policy, against a fork of Robinhood Chain, with every outcome read back from the chain directly: an address with seven open approvals of all four kinds listed, priced to the cent against an independent sum, and ranked plain-wallet first; six of them shut by a wallet that cannot batch (five transactions, one of them a single Permit2 lockdown) and by one that can (one atomic request), with the unticked Permit2 approval left exactly as it was; forty-five approvals shut in two batched requests; and a wrong wallet, a cancelled request, an empty address and a malformed one each refused by name.