๐ The Blaze Papers / The Great Revoker
๐งน The Great Revoker
Every approval your wallet has ever handed out on Sonic, what each one can actually take from you today, and a button to take it back. Here's exactly how it finds them, what it can't see, and why clearing ten approvals still costs ten signatures.
What an approval actually is
๐ฅฑ degen tl;dr
Approving a dapp isn't a one-time "yes" โ it's a standing permission that outlives the transaction you signed it for. You click once, the dapp gets to move up to that much of your token forever, until something explicitly turns it back off. Most people never turn it back off.
๐ค the nerd stuff
An ERC-20 approve(spender, amount) call writes directly into the token
contract's own allowance[owner][spender] storage slot. There's no expiry
field anywhere in the standard โ that number sits there exactly as written until a
later approve() overwrites it (spending doesn't zero it either;
transferFrom just decrements it).
NFT approvals work the same way in
spirit: setApprovalForAll(operator, true) flips a permanent boolean, and
a single-token approve(spender, tokenId) names one spender for one token
until reassigned. Every one of those states persists on-chain whether or not the dapp
that asked for it is still even running.
How the Revoker finds them
๐ฅฑ degen tl;dr
Two log sweeps to find every approval you've ever granted, then a live on-chain check per hit to see which ones are still real. Logs discover, calls decide. An approval you already revoked months ago is still sitting in the log history forever โ the live check is what drops it before it ever reaches your screen.
๐ค the nerd stuff
Discovery runs two eth_getLogs calls, each filtered to one event topic0
(Approval or ApprovalForAll) and to your
address as the indexed owner topic, over the whole chain from block zero. Owner-
filtering is what keeps each call small โ a few dozen logs in well under a second โ
rather than the collection-wide scans that blow the public RPC's result cap (see
docs/measured-facts.md ยง3).
Every hit becomes a candidate,
deduped to the newest log per (kind, token, spender, tokenId). Candidates then go
through verify(), one live eth_call each โ
allowance(), isApprovedForAll(), or getApproved()
depending on kind.
Anything that reads back zero, false, or a different spender is dropped outright. Only what verify() confirms is still live ever gets enriched and rendered.
The ERC-721 collision
๐ฅฑ degen tl;dr
ERC-20 token approvals and ERC-721 NFT approvals emit an event with the exact same name and signature, so a log-based sweep sees them as identical at first glance. Confuse the two and a token ID renders as if it were a spending allowance โ nonsense. The Revoker tells them apart by counting topics, not by guessing.
๐ค the nerd stuff
Approval(address,address,uint256) hashes to the identical topic0 whether
the emitting contract is an ERC-20 or an ERC-721 โ the event signature text is
byte-identical either way, so the hash can't discriminate.
The discriminator is topic
count: ERC-721 indexes tokenId as a topic, giving the log
4 topics total; ERC-20 leaves the value in the unindexed data payload, giving it 3.
classifyLog() branches on topics.length โ never on an
interface probe, which would cost an extra round trip per candidate for something the
log itself already tells you for free.
What "no contract code" means
๐ฅฑ degen tl;dr
Sometimes the address you approved has no bytecode at all. That's either a dead contract โ one that self-destructed or was never really deployed the way you thought โ or it's just somebody's plain wallet. The Revoker names both readings and asserts neither, because the on-chain data genuinely can't tell them apart.
๐ค the nerd stuff
Every spender gets one eth_getCode call. An empty result ("0x")
means there is no bytecode at that address right now โ full stop. It does not mean
"safe" and it does not mean "scam": a genuinely dead contract and a plain externally-
owned account produce the identical empty response.
The row gets a "โ ๏ธ no contract code here" badge either way, and it ranks at the very top of the risk ordering, because the second reading โ an ordinary wallet holding an open approval โ is exactly what a drainer looks like from the chain's point of view.
A read failure on
getCode assumes code exists rather than flagging a false alarm โ the badge
only ever fires on a confirmed empty result.
Balance at risk, not raw allowance
๐ฅฑ degen tl;dr
A scary-looking unlimited allowance on a wallet holding zero of that
token exposes exactly nothing today. The Revoker ranks and displays what's actually at
risk right now โ min(allowance, balance) โ not the raw number you signed,
which can make an empty bag look like the biggest threat on the page.
๐ค the nerd stuff
For an ERC-20 row, atRisk = min(allowance, balance) โ you cannot lose what
you do not hold, so a huge approval on a near-empty wallet is capped by the balance,
not the allowance.
For NFT-shaped rows there's no partial exposure to cap: an
operator (setApprovalForAll) approval exposes your whole
collection balance in that contract, and a single-token approval exposes exactly the
one tokenId it names โ it can never touch the rest of your holdings in
that collection, and the page's copy says so explicitly rather than implying the whole
collection is on the line.
Why there is no mass revoke
๐ฅฑ degen tl;dr
There's no "revoke all" button, and there never will be one. approve()
only ever listens to whoever calls it โ your wallet, and only your wallet โ so ten
approvals means ten separate signatures, one transaction each. Any tool that claims it
can clear a pile of approvals in one signature is overselling what the standard allows.
๐ค the nerd stuff
approve() and setApprovalForAll() are both
msg.sender-scoped by the ERC-20/ERC-721 standards themselves โ there is no
batched or delegated form defined anywhere in either spec, and a token contract has no
reason to add one just for revocation.
The revoke queue reflects that constraint
honestly: it walks one signature at a time, waits for each transaction to land, then
re-reads the exact same call verify() uses to confirm the allowance is
actually gone before moving to the next row.
A rejection halts the walk with the remainder untouched on screen โ nothing auto-continues, and nothing is ever sent you didn't individually sign for.
The Permit2 blind spot
๐ฅฑ degen tl;dr
Stated plainly: Uniswap's Permit2 holds allowances inside its own
contract, separate from the token's ordinary allowance mapping. The
Revoker can show you that you approved Permit2 itself โ it cannot see what Permit2 is
allowed to do downstream on your behalf. Anyone claiming a complete picture of your
approvals without a dedicated indexer is overselling it, and that includes us.
๐ค the nerd stuff
A standard ERC-20 approve() to the Permit2 contract address shows up in
the sweep exactly like any other row โ that part is fully visible. What's invisible is
everything inside Permit2: its own per-spender, per-token, time-bound
allowances live in Permit2's storage, granted by off-chain signatures that never touch
the ordinary Approval event topics this page filters on.
Seeing those would mean reading Permit2's own mappings directly (or running an indexer built for it), which is out of scope for a two-log-sweep tool. Revoking your approval to Permit2 itself removes Permit2's ability to move funds it doesn't already have standing permission for, but any permission already granted downstream through it needs to be managed on its own terms.