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
| Party | Trusted with | If it misbehaves |
|---|---|---|
| The contract code | all escrowed funds | funds can be lost. It is small and tested |
| The admin | tuning the fee (max 1%), the caps, and pausing new links | it 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 wallet | receiving swept fees and running the burn | fees could be mishandled. That affects the token's buyback, not users' escrowed funds |
| The web app | showing the right thing and generating the secret in the browser | a malicious copy could steal a secret on that device. Check the domain and the contract address |
| A relayer | submitting a signed claim | it can delay or refuse, never redirect |
| The token you send | behaving like a standard ERC-20 | see 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
feeBpslater does not affect open links. Test: create at 0.3%, raise to 1%, claim, and the fee is still 0.3%. - Hard fee ceiling.
setParamsreverts 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
sare rejected (malleability), the length must be 65 bytes,ecrecoverreturning zero is rejected, and the EIP-712 domain includes the chain id and contract address. - No upgradeability. No proxy, no
delegatecall, noselfdestruct. The fee wallet isimmutable.
#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
| Case | What happens |
|---|---|
| Undo and claim in the same block | Whichever is ordered first wins. The other finds no link and reverts. Funds are never paid twice |
| Link leaks before it is claimed | Whoever holds it can sign a claim for any recipient. The sender should undo immediately. Per-link caps limit the damage |
| Sender loses the link | Undo 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 id | They can undo, but they cannot re-share the link. Create a new one |
| Sender is a contract that rejects ETH | undo 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 ETH | the claim reverts with TransferFailed and the link stays open. Claim to another address |
| Re-entrancy from a malicious recipient or sender | A 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 token | Handled. 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 pausing | Unsafe, see below |
| Rebasing tokens | Unsafe, see below |
| Tiny sends | The 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 id | Reverts with LinkExists. The app should use a fresh random id |
| ETH sent directly to the contract | Rejected. There is no receive() or fallback() |
| A chain fork that changes the chain id | The 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 behaviour | Risk |
|---|---|
| The issuer can blocklist or freeze an address | if 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 transfers | payouts revert while paused. Funds are not lost, but they are stuck for the duration |
| Rebasing or elastic-supply tokens | the 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-transfer | handled, but each hop costs the sender |
| Non-standard return values | handled: 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 can | The admin cannot |
|---|---|
| set the fee for new links, 0 to 100 bps | take or move escrowed funds |
| change the ETH cap and the token cap | change the fee of an open link |
| pause creating new links, and unpause | block 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.