03 Start

Quickstart

Create, claim and undo a link from a script in under five commands, or try the same flows in the browser lab.

#Two ways to try it

In the browser, no setup. Open the lab. It runs the contract's rules locally with real keys and signatures. Create a link, click Undo or Claim as Friend, then use the attack buttons.

From a script, on a local chain. The repository contains the Solidity contract and its tests. You need Foundry (forge, anvil) and Node 20 or newer.

#1. Run the tests

cd contracts
forge test                      # 30 tests: unit, fuzz, re-entrancy
forge test --match-contract OopsendForkTest \
  --fork-url https://rpc.mainnet.chain.robinhood.com   # real USDG on chain 4663

The first command needs no network. The second forks Robinhood Chain and runs a full create, undo, create, claim round trip with the real USDG token.

#2. Start a local chain and deploy

anvil --chain-id 4663 --port 8545
forge create src/Oopsend.sol:Oopsend --rpc-url http://127.0.0.1:8545 \
  --private-key $DEPLOYER_KEY \
  --constructor-args $ADMIN $FEE_WALLET 30 2000000000000000000 100

The five constructor arguments are the admin, the fee wallet, the fee in basis points (30 = 0.3%), the ETH cap per link in wei (2 ETH here), and the token cap in basis points of total supply (100 = 1%). The fee wallet is immutable, so choose it with care.

This is the same flow the site runs. It uses ethers v6.

import { ethers } from 'ethers';

const provider = new ethers.JsonRpcProvider('http://127.0.0.1:8545');
const oops = new ethers.Contract(OOPSEND, ABI, sender);          // ABI: see "Integrate"

// the sender's browser makes a throw-away key; only its address goes on chain
const claimWallet = ethers.Wallet.createRandom();
const id = 48213n;                                                 // any unused uint64
await (await oops.createLinkEth(id, claimWallet.address, 7 * 86400,
  { value: ethers.parseEther('1.5') })).wait();

// the recipient signs (linkKey, recipient) with the key from the link: no gas needed
const key = await oops.linkKey(sender.address, id);
const sig = await claimWallet.signTypedData(
  { name: 'Oopsend', version: '1', chainId: 4663, verifyingContract: OOPSEND },
  { Claim: [{ name: 'linkKey', type: 'bytes32' }, { name: 'recipient', type: 'address' }] },
  { linkKey: key, recipient: BOB });

// anyone relays it; Bob never needs ETH
await (await oops.connect(relayer).claim(sender.address, id, BOB, sig)).wait();

The full, executed version of this script is in Integrate and its output is in Examples.

#4. Undo instead

await (await oops.undo(id)).wait();      // 100% back to the sender, no fee

undo takes only the link id, because the contract looks the link up under msg.sender. Nobody else can find your link by guessing.

#5. Let it expire

await (await oops.connect(anyone).expireRefund(sender.address, id)).wait();   // reverts NotExpired before the deadline

#Using the app

The site has real chain pages for this flow: app.html for the sender (create, list, undo with a wallet) and claim.html for the recipient. They talk to the contract address configured in site/assets/config.js through your wallet. You can attack the same rules in the lab without a wallet.