15 Trust

Security and limits

The trust model, what the contract guarantees and how that is tested, the edge cases, the admin's powers.

#Trust model

PartyTrusted withIf it misbehaves
The contract codeall escrowed fundsfunds can be lost. It is small and tested
The admintuning the fee (max 1%), the caps, and pausing new linksit can raise the fee for new links up to 1%, lower the caps, or stop new links. It cannot touch open links or escrowed funds
The fee walletreceiving swept fees and running the burnfees could be mishandled. That affects the token's buyback, not users' escrowed funds
The web appshowing the right thing and generating the secret in the browsera malicious copy could steal a secret on that device. Check the domain and the contract address
A relayersubmitting a signed claimit can delay or refuse, never redirect
The token you sendbehaving like a standard ERC-20see the token table below

#What the contract guarantees

These are properties of the code in contracts/src/Oopsend.sol, and each has tests.

  • Funds leave an escrow in three ways only: to the recipient named in a valid claim signature (net of the fee), to the fee accrual, or back to the sender. No function pays anyone else. There is no withdraw, no rescue, no sweep of arbitrary balances.
  • Single use. Every terminal function deletes the record before it pays. A second claim, undo or refund finds nothing and reverts with NoLink.
  • The recipient cannot be changed by a relayer. The claim signature covers the recipient. Test: a signature made for Bob, replayed with the recipient set to someone else, reverts with BadSignature.
  • Only the sender can undo. The record is looked up under msg.sender, so a stranger's call finds nothing.
  • Undo always works while the record exists, before or after expiry, and is not affected by pausing, by the fee, or by the caps.
  • The fee is locked at creation. Changing feeBps later does not affect open links. Test: create at 0.3%, raise to 1%, claim, and the fee is still 0.3%.
  • Hard fee ceiling. setParams reverts above 100 basis points.
  • Conservation. The escrow never holds less than it owes. Tested as fuzz assertions on the Solidity side: after a claim, the contract's ETH balance equals the accrued fees; after an undo, the sender is made whole to the wei. The JavaScript suite also checks that, across many mixed operations, the escrow's balance equals the sum of open links plus unswept fees.
  • Signature hygiene. Signatures with a high s are rejected (malleability), the length must be 65 bytes, ecrecover returning zero is rejected, and the EIP-712 domain includes the chain id and contract address.
  • No upgradeability. No proxy, no delegatecall, no selfdestruct. The fee wallet is immutable.

#Tested at the time of writing

  • 30 Foundry tests: unit, fuzz and re-entrancy, all passing.
  • 1 fork test against the real USDG token on Robinhood Chain (create, undo, create, claim), passing.
  • 15 JavaScript tests that reproduce Solidity vectors byte for byte.

#Edge cases

CaseWhat happens
Undo and claim in the same blockWhichever is ordered first wins. The other finds no link and reverts. Funds are never paid twice
Link leaks before it is claimedWhoever holds it can sign a claim for any recipient. The sender should undo immediately. Per-link caps limit the damage
Sender loses the linkUndo needs only the id, not the secret. The id is in the LinkCreated event, so a sender can list their own links from chain data and undo from any device
Sender loses the secret but not the idThey can undo, but they cannot re-share the link. Create a new one
Sender is a contract that rejects ETHundo and expireRefund revert with TransferFailed, and the funds stay safe in the escrow until that address can receive ETH. Use a plain account as the sender
Recipient is a contract that rejects ETHthe claim reverts with TransferFailed and the link stays open. Claim to another address
Re-entrancy from a malicious recipient or senderA lock blocks any call back in, and the record is deleted before any payout. Both are tests: a re-entering recipient and a re-entering sender are both refused
Fee-on-transfer tokenHandled. The contract records the balance difference after pulling the tokens, so it only owes what it received. The sender bears the token's own transfer fee
Tokens with blocklists, freezing or pausingUnsafe, see below
Rebasing tokensUnsafe, see below
Tiny sendsThe fee rounds down to zero below 334 base units at 0.3%. Gas is a fixed cost, so very small sends are not economical
Duplicate link idReverts with LinkExists. The app should use a fresh random id
ETH sent directly to the contractRejected. There is no receive() or fallback()
A chain fork that changes the chain idThe contract recomputes the EIP-712 domain, so old signatures are invalid on the new chain

#Tokens you should not send

The contract assumes a standard ERC-20. These break that assumption.

Token behaviourRisk
The issuer can blocklist or freeze an addressif the contract's address is frozen, no link of that token can pay out until it is unfrozen. If a recipient or the sender is blocklisted, that payout reverts
The issuer can pause transferspayouts revert while paused. Funds are not lost, but they are stuck for the duration
Rebasing or elastic-supply tokensthe contract records a fixed amount while the real balance changes. After a negative rebase it can owe more than it holds, and the last claimant bears the loss
Fee-on-transferhandled, but each hop costs the sender
Non-standard return valueshandled: both "returns nothing" and "returns bool" are accepted

The per-link token cap (1% of total supply by default) limits how much any one link can hold.

#Admin powers, exactly

The admin canThe admin cannot
set the fee for new links, 0 to 100 bpstake or move escrowed funds
change the ETH cap and the token capchange the fee of an open link
pause creating new links, and unpauseblock undo, refund, claim or sweeping fees
hand the role to another address, or renounce it with transferAdmin(address(0))change the fee wallet, which is immutable
upgrade or replace the code

#Known limits

  • Live on Robinhood Chain. See Risks for the risks that come with any new contract.
  • The undo window is bounded by the expiry you choose, between 10 minutes and 90 days. A link left open past the expiry can still be undone by its sender, but it can no longer be claimed.
  • After a claim there is no reversal. That is the design, not a flaw.
  • Privacy is limited. The sender address, amount, token and expiry of every link are public events. The recipient becomes public when the claim lands. Oopsend hides the secret, not the transfer.
  • A link is a bearer instrument until it is claimed or undone. Treat it like cash.
  • The JavaScript lab is a simulation. It teaches the rules. It is not evidence about the live chain.