05 Contract
Architecture
The pieces of the system, what each one is trusted with, and how the JavaScript engine is kept honest against the Solidity.
#The pieces
┌──────────────────────┐ ┌─────────────────────────┐
│ Web app (static) │ │ Relayer / undo bot │
│ home, lab, docs │ │ (designs, not running) │
│ • ethers │ │ • submits signed claims │
│ • secret stays local │ │ • points to undo page │
└──────────┬───────────┘ └────────────┬────────────┘
│ signed transactions │ transactions the user or a relayer signs
▼ ▼
┌───────────────────────────────────────────────────────┐
│ Robinhood Chain (4663): Oopsend.sol │
│ links[linkKey] · feesAccrued[token] · immutable fee wallet │
└───────┬───────────────────────────────┬───────────────┘
│ events │ sweepFees (anyone)
▼ ▼
┌────────────────┐ ┌─────────────────────────┐
│ Indexer / app │ │ Fee wallet (public) │
│ "My links" │ │ buy $OOPS → burn │
└────────────────┘ └─────────────────────────┘
#Components and trust
| Component | What it does | Trust required |
|---|---|---|
| Oopsend.sol | Holds escrowed funds and is the only thing that can move them | None beyond the code: no proxy, no admin withdrawal |
| Web app | Generates the secret, builds links, signs claims in the browser | The page you load could be malicious, so check the domain. It cannot move funds without a signature you approve |
| Relayer | Submits a signed claim and pays gas | None for safety: it cannot change the recipient. It can delay or refuse, and you can submit yourself |
| Undo bot | Replies to a tweet with a link to the undo page | None: it holds no keys, it only points |
| Indexer | Rebuilds "My links" from events | None for funds. If it is down you lose convenience, not money |
| Fee wallet | Receives swept fees, buys back and burns $OOPS | A team-run wallet in V1: burns depend on the team executing them |
#Off-chain data
None is required. A link is self-describing: the sender and the id are in the path, the secret is in the fragment, and the escrow record is readable with one call, links(linkKey(sender, id)). The app can list a sender's links from LinkCreated events filtered on the indexed sender topic. The event carries the link id, so a sender who switched device can still undo(id) any open link. Only the secret, needed to re-share a link, is not recoverable from chain data.
#Why a plain contract
Oopsend is one contract with no proxy, no hooks, and no external calls except token transfers and ETH payouts. Every function either creates a record, deletes a record while paying it out, or tunes parameters. That is a deliberate shape: a small surface is easier to read and to test.
#The JavaScript engine
The lab and the live attacks on the home page run site/assets/engine.js, a JavaScript port of Oopsend.sol. It has the same storage, the same checks in the same order, the same custom-error names and 4-byte selectors, and the same EIP-712 digest. It uses real secp256k1 keys and signatures through ethers. What it does not have is a chain: the ledger is local memory and the clock is simulated.
A port is only worth something if it cannot drift. Two safeguards:
- Shared vectors. The Solidity suite writes
contracts/test/vectors.json: the EIP-712 domain separator, six link keys, six digests and six signatures, plus error selectors.tests-js/engine.test.mjsrecomputes all of them in JavaScript and asserts equality. ECDSA signing in ethers is deterministic (RFC 6979), so even the signature bytes match Foundry's. - Mirrored scenarios. The JS suite replays the same scenarios as the Solidity suite: claim, undo, expiry, front-run, wrong key, caps, pause, fee locked at creation, fee-on-transfer, and a conservation check (the escrow's balance equals open links plus unswept fees).
At the time of writing: 30 Foundry tests plus 1 fork test pass, and 15 JavaScript tests pass.
#Repository layout
| Path | Contents |
|---|---|
contracts/src/Oopsend.sol | the contract |
contracts/test/Oopsend.t.sol | Foundry unit, fuzz, re-entrancy and fork tests |
contracts/test/vectors.json | vectors written by Solidity, read by JavaScript |
site/assets/engine.js | the JavaScript port |
tests-js/engine.test.mjs | parity tests for the port |
scripts/docs-e2e.mjs | a real end-to-end run on a local anvil, used by these docs |
#Deployment
| Chain | Robinhood Chain, id 4663 |
| Contract | 0x7B530e91D9dc24C78Ca2eEE2B494E7dE1B680AC0 |
| Deploy block | 90156561 |
| Deploy transaction | 0x877015a2d1ff4810350bd413ab139e409a35a82a98e495c08c1311ccaca2da75 |
| Admin | 0x59B6919AdC07a3b6a6D7b91f7773A721bD0c6c73 |
| Fee wallet (immutable) | 0x59B6919AdC07a3b6a6D7b91f7773A721bD0c6c73 |
| Fee, caps | 30 bps, 2 ETH per ETH link, 1% of total supply per token link |
| Explorer | robinhoodchain.blockscout.com/address/0x7B530e91D9dc24C78Ca2eEE2B494E7dE1B680AC0 |
Check this address against the one on the home page and in the site header before approving anything. The same file is in the repository as contracts/deployment.json.