Q-ACCOUNTS
//01_ WHAT IT IS
A Q-Account is a second trading account, a small contract that the exchange treats like any trader. It has no ECDSA owner. The only thing that can move what it holds is a Winternitz one-time signature made with a key kept in your browser, and the contract checks that signature itself.
Wallet keys are ECDSA keys. A large enough quantum computer running Shor's algorithm could recover one from its public key, which is on chain once the wallet has sent a transaction. Winternitz signatures are made of keccak-256 hash chains. The best known quantum attack on a hash is Grover's search, which leaves 128 bits of security. It is the same family as SLH-DSA, the hash-based standard NIST published in 2024 (FIPS 205).
//02_ OPENING ONE
- 01In the app, open the
Q-Accounttab and pressCreate a Q-Account. Your browser draws a 32-byte secret from its secure random source and shows it as 24 words. - 02Write the 24 words down, in order. They are the only way to recover the account: nobody can reset it, not us and not your wallet.
- 03Choose
Q-Accountas the account you trade with, then deposit from theAccounttab. Your wallet signs a USDG permit for the factory and sends one transaction that creates the account, moves the USDG into its exchange balance and gives it 0.00004 ETH for its one-click key.
The account address is fixed by its first key (CREATE2 with salt keccak256(seed, keyHash)), so it is known before it exists. Later deposits go through fundWithPermit. USDG sent straight to the address is moved into trading by sweep(), which anyone may call: it can only move funds into the account's own exchange balance.
//03_ ONE SIGNATURE
The parameters: w = 16, n = 32 bytes, keccak-256, 67 chains of 15 steps. Each step is a distinct hash:
F(seed, adrs, x) = keccak256(seed || adrs || x), adrs = ("PHQK" << 224) | (key << 32) | (chain << 8) | step- 01The digest binds everything:
keccak256(TYPEHASH, chainId, account, keyIndex, nextKeyHash, gasMin, keccak256(calls)). - 02Its 64 hex digits di pick a position on chains 0 to 63. The checksum
c = sum(15 - d_i), at most 960, is written as 3 more digits for chains 64 to 66. Raising any digit lowers the checksum, so a forger would need to go down a chain, which means inverting keccak-256. - 03Chain i of key k starts at a secret
sk = keccak256("paperhands.q.sk" || master || k || i). The signature word is that secret hashed di times. 67 words, 2,144 bytes. - 04The contract hashes each word the remaining
15 - d_itimes and hashes the 67 ends together. The result must equalkeyHash, the hash of key k's public key that the account stores. - 05Then it stores
nextKeyHashand moves to key k + 1. The next key was part of the signed digest, so nobody can swap it.
A Winternitz key is safe for one signature only. The contract enforces it by rotating on every verified signature. The app enforces the other half: it keeps each signed batch until it lands and refuses to sign a second one with the same key. If a batch does not land, the same batch is sent again.
//04_ WHAT NEEDS THE QUANTUM KEY
| ACTION | SESSION KEY | QUANTUM KEY |
|---|---|---|
requestOpen, requestClose | yes, for the Q-Account | yes |
withdraw | no | yes |
collectClaims, claimRewards | no | yes |
authorizeSession, revokeSession | no | yes |
any transfer out of the account | no | yes |
A withdrawal is one signed batch of two calls: withdraw(amount) on the exchange, then a USDG transfer to your wallet. The batch runs all or nothing. If it fails on chain (more than your available balance, say), the key is still spent and nothing moves; you sign the next batch with the next key.
//05_ WHO PAYS THE GAS
The keeper relays signed batches and pays their gas, at https://paperhands-keeper.fly.dev/q/relay. The signature is the only authority: the relay cannot change a byte of the batch, and anyone may relay the same payload. The relay serves accounts of this factory, batches that call the exchange, USDG and the token, with hourly limits. When it refuses or is down, the app asks your wallet to send the batch; the wallet then pays the gas and nothing else.
A batch carries a signed gas floor, gasMin, so nobody can relay it with too little gas to make it fail.
//06_ CONTRACTS
- //Q-ACCOUNT FACTORY_
- 0xab7d7b7A633313F78B72B5d42aDE2a25d4D70bdA
The factory has no owner and holds nothing. It deploys accounts, funds them in one transaction, and counts the post-quantum signatures they verify (signatureCount()). Each account stores its seed, its current key hash and its key number, and exposes execute(calls, nextKeyHash, gasMin, sig), sweep() anddigest(...). Neither contract can be upgraded.
//07_ LIMITS
- //The key lives in this browser_
- In localStorage under ph.q.<wallet>, never sent anywhere. Malware on your device could read it.
- //Lose the words, lose the account_
- Without the 24 words and this browser, nothing can move the funds again.
- //One device at a time_
- Two devices signing with the same key at once could reveal two signatures of one key.
- //No external audit_
- Tested with unit tests, fuzzing and a mainnet fork against the live exchange.
Credits: hands from Michelangelo, The Creation of Adam (detail), public domain, via Wikimedia Commons. Marks of the markets and integrations belong to their owners (sources, credits).