Built for Agents Onchain — KeeperHub × DoraHacks

Revoker

Lands the revoke before the drainer moves.

$revoker watch

TypeScript · Solidity 0.8.28 + Foundry · viem · KeeperHub · Ethereum Sepolia

The most common wallet-drain vector

approve(spender, MAX_UINT256)

You signed it once, months ago. It never expires — and the day that contract turns malicious, the allowance is still live.

The problem

Every tool stops at the alert.

Approval checkers are read-only: they rate the risk, list the allowance — and leave the acting to you. For a wallet holder with forgotten approvals, or a DAO treasury signer asleep at 3 a.m., an alert is not protection.

APPROVAL RISK — HIGH

A spender you approved has been flagged. Your allowance is unlimited.

allowance = MAX_UINT256 · granted 214 days ago

Acknowledge

The state of the art ends here — a notification, and a button that does nothing on-chain.

The solution

Revoker acts.

An autonomous agent that watches a wallet’s live approval set and executes approve(spender, 0) through KeeperHub the instant a threat rule fires — no human in the loop.

revoker — watch · Ethereum Sepolia
$ revoker watch
watching 0x5E2e…bab7 · 3 rules armed · signer: Turnkey enclave via KeeperHub
[detect]  MockUSDC approval → 0x8eBf…892e · allowance = MAX_UINT256
[rule]    unlimited-to-unverified — FIRED (spender source unverified)
[rule]    denylisted — FIRED (known-bad spender)
[execute] KeeperHub check-and-execute → approve(spender, 0)
[confirm] 0x15f0f816…541a82 · allowance = 0 · sponsored · no human involved

Proof of execution · Ethereum Sepolia

A real drain, really stopped.

The trap

Victim grants unlimited approval

approve(spender, MAX_UINT256) to the drainer contract.

0xeb4243d1…fe1113 allowance = 1.157e77
The response

Revoker fires — no human involved

approve(spender, 0) via KeeperHub check-and-execute.

0x15f0f816…541a82 allowance = 0
The verdict

The drainer fires anyway

The drain tx succeeded — it just found nothing left to take.

0x5579da99…6e1e4d drained: 0

We let the drain contract fire after the revoke. It succeeded — and

took 0

balance unchanged: 10,000 mUSDC
across the whole sequence

Watch the 2:34 demo ↗ The recorded run of this exact cycle — the drain fires at the end, and takes nothing.

How it works

One loop. No human.

Wallet approval state Ethereum Sepolia approvals · balances · code watched: 0x5E2e…bab7 poll Revoker agent watcher — live approval set rules — 3 auditable checks planner — approve(spender, 0) TypeScript · viem · holds no key execute KeeperHub ONE ATOMIC OPERATION re-read allowance — live chain execute approve(spender, 0) Turnkey enclave sponsored relay signing never leaves the enclave tx The revoke lands Ethereum Sepolia allowance → 0 gas 46,482 (p50) audit trail — execution id · status · gas · sponsorship → local /verify dashboard (SSE)

The sharpest technical idea

Nothing to front-run.

read-then-write a typical agent

read allowance→ 1.157e77
front-run window A mempool-watching drainer slips a transferFrom in here — before the revoke lands.
approve(spender, 0)
Decides at time T, acts at T+n. The window is the attack surface.

check-and-execute Revoker, via KeeperHub

read allowancelive chain state
approve(spender, 0)
one atomic server-side operation
Same two steps, no gap between them. Nothing to front-run.
KeeperHub’s receipt from the reference run — the condition re-read server-side at execution time: observedValue: 115792089237316195423570985008687907853269984665640564039457584007913129639935 = 2256 − 1 — MAX_UINT256, read from the live chain, not passed in by this process.

Integration depth

KeeperHub is the engine.

10 API surfaces 12 call sites 3 threat rules

Remove KeeperHub and Revoker needs seven separate systems — relayer, gas oracle, MEV-protected route, confirmation poller, action discovery, ABI resolution, audit pipeline — plus custody. The atomic check-and-execute and the enclave-held key are the two pieces a standalone script cannot honestly replicate.

No key to steal. Signing happens inside a Turnkey enclave via KeeperHub — the agent process never holds a private key.

POST /execute/check-and-executethe atomic revoke
POST /execute/contract-callcontract writes
POST /execute/transfernative transfers
GET  /execute/{id}/statusconfirmation · gas · audit record
GET  /chainsnetwork + explorer resolution
GET  /chains/{id}/abisource-verification signal, rule 1
GET  /user/walletsigner identity assertion
GET  /user/wallet/balancestoken discovery
simulate: truepre-flight dry runs
Idempotency-Keysafe retries, no double-execution

Reliability · 25 full cycles against live Sepolia

Measured, not claimed.

13.47smedian detect → revoke
25.17sp95
25/25cycles succeeded
46,482gas per revoke (p50 = p95)

Every bar is a real transaction. Cycles 10–13 hit a slow block-inclusion window (amber) — the network’s variance, reported as a distribution, not hidden behind an average.

86 tests — 44 TypeScript · 42 Solidity 100% Solidity coverage — lines · statements · branches · functions

Scope · stated plainly

Known limits, on the record.

A security tool that hides its blind spots is part of the problem. These are Revoker’s.

Sepolia testnet only

The mainnet path is documented, not executed. No real funds were risked producing any number in this deck.

Token discovery is watchlist-scoped

No public RPC serves an address-less eth_getLogs over a useful range. Revoker protects the tokens it is told to watch — not coverage it doesn’t have.

young-spender abstains loudly

Without an archive RPC it returns INDETERMINATE and names the remedy — instead of silently reporting “safe”.

The threat model is narrow on purpose

A spender that is verified, aged, and off the deny-list trips nothing. That case is out of scope, not silently mishandled.

The ask

Open the receipts.

Judge Revoker on execution — that is the point. Every number in this deck is a Sepolia transaction; the allowance we zeroed is still zero, and one cast call proves it.

# no credentials, no account, no trust in this deck required
cast call 0x4facb5FD1682c4449cAD42b7590861f7eD5c88Cb \
  "allowance(address,address)(uint256)" \
  0x5E2e5Fd3aD7fDC9B94482930db8b5F45E439bab7 \
  0x8eBf8540EdE8e40CD94825C418758d4029D8892e \
  --rpc-url https://ethereum-sepolia-rpc.publicnode.com

Expected output: 0

revoker.edycu.dev

The drainer fired.
It found an empty room.

v1.5.1
Speaker notes