Skip to content

goldk3y/quantum-bot

Repository files navigation

Quantum Bot

A local desktop bot for the QuantumRaffleV2 contract on Ethereum mainnet. Rust and React, packaged with Tauri 2.

Your key is generated, encrypted, and used to sign entirely on your own machine. There is no account, no cloud, and no backend, and the key itself is never sent anywhere. The one deliberate way it leaves the Rust process is Export, which re-prompts for your passphrase every time.

Built to docs/quantum-bot-prd.md. Where the implementation departs from that spec, and where the spec turned out to be wrong, is recorded in docs/findings.md.


Read this before you run it

The strategy this kind of bot is usually built around does not work.

Entering the raffle does not secure a win. It resets the full inactivity timer and makes you the one to beat for another 600 seconds. It is not a snipe. It is a bet that nobody responds.

We measured whether anybody responds. Across all 147 entries in games 1 to 3:

In 90 of 90 cases, a rival re-entered before the timer expired. A last-second entry has won 0% of the time.

The median gap between entries is 540 seconds against a 600 second deadline, which is the signature of a rival bot re-entering deliberately, just before each deadline. So the app defaults to simulation mode, and automatic mode is gated behind an explicit acknowledgement that shows you this number first.

Reproduce it yourself. This replays every GameEntered log since the deploy block, so it needs an archive endpoint with a key. The keyless default will rate limit partway through and abort:

node scripts/analyze-history.mjs "https://mainnet.infura.io/v3/<key>"

Status

Not production ready. Feature complete, and verified everywhere it can be verified without credentials or elapsed time. The milestones below are the exit gates from the spec.

Milestone State
M1 Read-only Verified 5/5 against live mainnet: live config, isWinner on every ticket of the open game, fee history, ticket reconstruction. A 24 hour soak is outstanding and needs a keyed RPC.
M2 Simulator Answered, and it failed the feature. 90/90 re-entry, so automatic mode ships gated rather than on. The gate failing is the gate working.
M3 Execution Verified 6/6 on a Foundry fork, including a real signed entry the chain confirms. The spec forbids mainnet first.
M4 Background Verified 3/3 over a websocket fork: tick on new heads, cancellation, backoff. A 72 hour unattended soak is outstanding.
M5 Distribution Blocked on an Apple Developer ID certificate and an update signing key. The updater plugin is deliberately not registered until both exist, because a placeholder key reads as configured while protecting nothing.

Requirements

  • Rust 1.82 or newer
  • Node 20.19+ or 22.12+ (the floor Vite 7 sets), and pnpm
  • macOS 10.15+ or Windows. On macOS you also need the Xcode Command Line Tools.
  • An archive capable Ethereum RPC endpoint with WebSocket support

The archive requirement is not optional. Ticket ownership can only be recovered from GameEntered logs, because the contract exposes no ticketsOf(address) view, so the bot has to read log history. Measured against real providers (docs/findings.md §9):

Provider Works?
Infura, dRPC Yes
Alchemy free tier No. 10 block cap on eth_getLogs
publicnode No. Archive requests need a token
Cloudflare No. Rejects the range as too large

Quick start

pnpm install
pnpm tauri dev

Then set your RPC endpoint in Settings and press Test connection before starting the bot.

Configuration

Everything the app needs is set in the UI. Two things are worth knowing about what happens on disk.

Where your data lives. On macOS, under ~/Library/Application Support/com.quantumbot.desktop/:

File Contents
vault.json Your encrypted key. Written atomically, fsynced, 0600. Nothing else has a copy.
settings.json Plaintext configuration and spend accounting. Never secrets.

Back up vault.json. If you lose it, the key is gone. The app refuses to create a wallet over a vault file it cannot read, rather than silently replacing it, so a corrupt file is recoverable from a backup and unrecoverable without one.

QUANTUM_BOT_TEST_RPC points the scripts, the fork, and the live tests at an endpoint of your choice. It takes an HTTP URL, unlike the app's own endpoint, which is a WebSocket one. Set it: the fallbacks are keyless public endpoints that rate limit, and they will make the network tests flake rather than fail honestly.

export QUANTUM_BOT_TEST_RPC="https://mainnet.infura.io/v3/<key>"

Each script also takes the URL as its first argument, which overrides this.

Testing

The default run is fast and offline. Everything that touches the network or needs a fork is #[ignore]d, so nothing in a normal run can reach the chain.

cd src-tauri && cargo test --lib   # 172 unit tests, no network
pnpm check                         # tsc, plus the Rust to TypeScript contract

pnpm check is the one guarding the seam. It regenerates src/generated/ from the Rust types and fails if the checked-in files move, so a change to a DTO cannot land without the TypeScript following it. Generation only helps if drift breaks a build: for a while this repo generated those files and imported none of them, and the types drifted anyway.

The opt-in suites, in rising order of what they touch:

# Read-only checks against live mainnet (M1)
cargo test --test live_mainnet -- --ignored --nocapture

# The full RFC 9106 2 GiB vault derivation (~3s in release, ~50s in debug)
cargo test --release --lib -- --ignored

# End-to-end entry on a local fork (M3). The only tests that sign anything.
./scripts/local-fork.sh                  # in one terminal
cargo test --test fork_entry -- --ignored --nocapture --test-threads=1
cargo test --test runtime_loop -- --ignored --test-threads=1

Never point the signing tests at mainnet. scripts/local-fork.sh passes --chain-id 1 so the fork presents as mainnet and the bot's allowlist accepts it, which is exactly what makes it a useful test and exactly what would make a mistake here expensive.

Verify the contract bindings against the live deployment:

node scripts/verify-abi.mjs   # ABI shape and revert selectors vs the chain

How it is built

src/                 React + TypeScript. Renders state, sends intent.
  generated/         Wire types, generated from the Rust structs. Do not edit.
  components/        Dashboard, settings, onboarding, activity feed.
  lib/               IPC wrapper, formatting, copy.

src-tauri/src/
  security/          vault (argon2id + XChaCha20-Poly1305), signer, hygiene
  blockchain/        bindings, atomic reader, ticket index, fees, submitter
  bot/               types, strategy, policy, runtime
  activity/          structured log
  commands.rs        the entire IPC surface
  state.rs           app state, including the spend journal
  storage.rs         vault and settings on disk

Three rules shape the whole thing.

The frontend is untrusted. It renders state and sends intent. It holds no key, signs nothing, and never talks to Ethereum. The CSP's connect-src is ipc: http://ipc.localhost and omits every RPC origin, so this is enforced rather than merely intended. The webview's capability grant is core:default and nothing else.

Strategy decides what is desirable. Policy decides what is allowed. They are separate modules that cannot import each other, and policy always runs last. This is enforced by the type system rather than by convention: policy::approve is the only constructor of PolicyApproval, and SignerHandle::sign_entry requires one. Code that skips policy does not fail review, it fails to compile.

Fail closed. Any uncertainty, whether a stale read, an unknown revert, a refused log scan, or a chain ID mismatch, skips the tick. A missed entry costs nothing. A wrong entry costs money.

Two details worth knowing

The countdown does not exist during warm-up. A game cannot end until it has minTicketsBeforeEnd tickets (currently 25), no matter how much time passes. Any countdown shown before then is fiction. Rather than guard against displaying one, the Phase type simply has no field for it outside Phase::Live, so the bug cannot be written.

Winning tickets are provisional. Every new entry shifts every ticket's position from the end, so tickets drift in and out of winning slots continuously. Only the state at game end pays, and the UI says so.

Security, honestly

The vault is argon2id (RFC 9106 first recommended: 2 GiB, t=1, p=4) over XChaCha20-Poly1305, falling back to the second recommended profile (64 MiB, t=3) only if 2 GiB cannot be allocated, and recording which profile it used. That protects your key against disk theft, backups, and stray copies.

It does not protect against software already running as you. The bot must decrypt to sign, so anything with your user account can get the key. No storage design fixes that.

What actually protects you is blast radius, and it is mostly things you do:

  • Use a dedicated hot wallet, funded only with working capital.
  • Route prizes to a cold wallet. This is the single most effective control here, and it is off until you set an address, because there is no sensible default: only you know your cold wallet. The dashboard warns until you set one. Once set, the hot key can never touch the winnings, even if it leaks later.
  • Keep the per-game and per-day spend caps meaningful. They are enforced by policy, persisted before any transaction is broadcast, and survive a restart.

One known gap, tracked under M5. security/hygiene.rs disables core dumps at startup, which works on any Unix and means a crash cannot write your key into /cores. It cannot stop a same-uid process from attaching a debugger: on Linux that is PR_SET_DUMPABLE, and macOS has no runtime equivalent. The macOS answer is the get-task-allow entitlement being absent from a signed, hardened-runtime build, which is a property of the bundle rather than something the process can set about itself. Builds are not signed yet, so that protection is not in force, and an unsigned download would fail Gatekeeper anyway. Run from source until it lands.

Disclaimer

This software signs financial transactions on Ethereum mainnet on your behalf. It is unaffiliated with the authors or operators of the QuantumRaffleV2 contract. It is provided as is, with no warranty. You are responsible for the funds in any wallet you give it. Read the strategy note at the top of this file before you enable automatic mode.

License

Unpublished. All rights reserved.

About

Local desktop bot app for Quantum Raffle.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors