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.
#3. Create a link, claim it with a relayer
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.
#What to read next
- Contract reference for every function and its checks.
- Link format to build links that wallets can parse.
- Errors for what each revert means.