Audit
Security
What the site does with your wallet, what it will never do, and the threat model for contracts that are still unwritten. Written honestly: where protection exists we say how, where it does not we say so plainly.
What this site never does
It never asks for your seed phrase or private key. It never requests
approve or setApprovalForAll on the collection. It never sends a transaction you did not click. It loads no third-party scripts. If any of that happens, you are not on our site.| What we do | Why | Risk |
|---|---|---|
| Read your connected address | show your pools | public data |
| Read balance and tokenURI | render the NFTs | view calls only |
| Ask for a plain signature | prove the address is yours | signatures cannot move funds |
| Open a pool (after deploy) | write state on-chain | a normal transaction you approve |
Connection is injected-wallet only — MetaMask, Rabby, Brave and compatible. We deliberately skip WalletConnect and social-login SDKs: each one is third-party code running on a page with wallet access. Fewer dependencies, smaller attack surface, and a strict CSP stays possible.
The ownership signature
The message is written so a human can read it in the wallet and understand it is not a transaction.
<domain> wants you to sign in with your wallet.
This signature proves you control this address.
It does NOT approve any transaction, token transfer or spending allowance.
App: Rain Pools
Address: 0x…
Nonce: <one-time>
Issued At: <ISO time>
Expires At: <ISO time>- Domain on the first line. If you are signing something with a different domain, it is phishing.
- One-time nonce. A signature cannot be reused for a new session.
- 12-hour lifetime. Expired sessions are dropped automatically.
- Verified by cryptography, not by trusting storage. On every visit the saved signature is re-checked with
verifyMessage. Faking a browser record achieves nothing.
An honest limitation right now
The nonce is generated in the browser, because the site has no backend. That is enough to prove address ownership on this device, but not enough for server authorisation — a server cannot confirm it issued that nonce. The moment there is an API holding money or privileges, the nonce must come from the server and be burned after first use. Today the signature unlocks nothing but the interface, so there is no exposure — but this must be closed before the first server endpoint.
Frontend hardening
| Measure | Status | Detail |
|---|---|---|
| Content-Security-Policy | done | no external scripts or frames, frame-ancestors none |
| Clickjacking | done | X-Frame-Options: DENY |
| HSTS | done | forced HTTPS, two years, preload |
| Referrer-Policy | done | your address never leaks in a referrer |
| Permissions-Policy | done | camera, mic, geolocation, payments all off |
| Analytics and trackers | done | none at all |
| External fonts and CDNs | done | none — the logo is SVG, fonts are system |
| Dependencies | partial | minimal set; still needs regular npm auditing |
Custody while staked
This is the most sensitive part of the project, so the decision favours safety over convenience: the NFT never leaves your wallet.
| Threat | How it is closed |
|---|---|
| Compromised staking contract drains the collection | Impossible — it holds no approval and cannot transfer. It only stores an “open” flag. |
| A bug locks NFTs forever | A hard ceiling on lock duration. Past it the lock stops applying, with or without the team. |
| Owner swaps staking for a malicious contract | The address is set once and becomes immutable. |
| Selling an NFT with an open pool breaks accounting | Transfers revert at the collection level while the pool is open. |
| Phishing site asks for approval “to stake” | The real site never needs approval. Any such request is fraud. |
Contract threat model
| Vector | Status | Mitigation |
|---|---|---|
| Reentrancy on claim | before deploy | checks-effects-interactions plus ReentrancyGuard |
| Validator manipulates weather | before deploy | commit-reveal instead of block values |
| Silent keeper freezes the game | before deploy | public fallback reveal on timeout |
| Keeper picks a favourable seed | before deploy | hash published before the event starts |
| Opening a pool on someone else's NFT | before deploy | ownerOf == msg.sender on every open |
| Exiting right before a drought | before deploy | 8-hour lock, early close reverts |
| Overflow in reward math | before deploy | Solidity 0.8 checks plus an emission cap |
| Unlimited reward minting | before deploy | hard cap on $RPOOL, vault is the only minter |
| Push distribution breaks on one recipient | before deploy | pull model |
| Gas bomb in batch calls | before deploy | array length limits |
| Silent upgrade | before deploy | non-upgradeable contracts, 48h timelock on parameters |
What “before deploy” means
These are designed, not implemented — the contracts do not exist yet. Nothing here should be read as audited until the code is written, tested and reviewed by someone else.
Checklist before mainnet
- Full test coverage: open, close, early-close attempts, opening someone else's token, silent-keeper behaviour.
- Fuzz the reward math over long event chains — accruals must never go negative or overflow.
- Static analysis (Slither, Aderyn) with every warning triaged.
- Full testnet run: mint, open, a streak of rain and drought, claim, close, resale.
- External review by someone who did not write the code.
- Source verification in the explorer before the first public sale.
- Server-issued, single-use nonce once an API exists.
- A funded bug bounty before rewards go live.
Rules for pool owners
- Check the domain before connecting. The first line of the signature must match the address bar.
- Rain Pools will never ask for a seed phrase, a private key, or
setApprovalForAll. Any such request is phishing, however convincing the page looks. - Read the whole signature text. A genuine login says in plain words that it is not a transaction.
- Keep valuable NFTs on a separate wallet and your main funds on hardware.
- While a pool is locked you cannot sell it. That is expected behaviour, not a failure.