Security
What has been tested, what the site checks, and what still rests on trust.
Audit status
The contracts have not been audited by a third party. The test suite described below is the project's own. It shows that the contracts behave as intended in the cases it tests, and an independent review could still find problems it misses.
What the tests cover
The suite runs on a local Hardhat network against a mock USDG with 6 decimals. Run it with:
cd contracts
npm install
npm test
| Area | What the tests check |
|---|---|
| Vault metadata and constructor | 18 decimals, USDG as the asset, name, symbol, cap and owner. A CapUpdated event at deployment. An asset without 6 decimals is rejected, and so is the zero address as owner. |
| Deposit and redeem | The first deposit mints 10^12 share units per USDG base unit. A deposit and redeem round trip returns the deposit with no fee. After yield, a round trip returns the deposit minus at most one base unit. mint and withdraw follow their preview functions. |
| Yield | addCapital and a plain USDG transfer both raise convertToAssets(1e18), and addCapital emits CapitalAdded. A zero amount and an empty vault are rejected. A later depositor buys at the current price and does not dilute earlier holders. |
| Deposit cap | maxDeposit is the cap minus totalAssets, and maxMint is its rounded-down conversion. A deposit above the room left reverts with ERC4626ExceededMaxDeposit, and an exact fill works. A mint above maxMint reverts with ERC4626ExceededMaxMint. Donations and settled rent count against the cap. A cap below totalAssets blocks deposits without making anything revert. A very large cap does not make maxMint revert. |
| Pause | Deposit and mint are blocked with the ExceededMax errors. redeem, withdraw, share transfers and addCapital keep working. Only the owner can pause, unpause and set the cap. |
| Inflation and donation attack | A victim who deposits after an attacker's donation gets fair shares, and the attacker loses money. Donating the victim's exact deposit amount cannot round the victim down to zero shares. |
| Owner limits | The vault exposes no state-changing function beyond ERC-4626, ERC-20 and the owner switches. The owner cannot move vault USDG or holders' vRENT. renounceOwnership reverts. Ownership transfer takes two steps. |
| RollRegistry | Starts empty. Only the owner can publish. Roll 1 stores every field, including the block timestamp, and emits RollPublished. Numbering is strict: publishing 2 before 1, publishing 0 and republishing 1 all revert. A zero hash is rejected. rolls(n) returns the word layout the site decodes. renounceOwnership reverts and ownership transfer takes two steps. No function can change or delete a published Roll. |
A seeded random test runs 160 operations by default across four holders: deposits, mints, redeems, withdrawals, addCapital calls and plain donations. After every step it checks that totalAssets() equals the vault's USDG balance, that the vault could pay every holder at once, that the share price has not gone down, and that no holder can take out more than they paid in plus their fair share of added capital and rounding dust. At the end it checks that USDG is conserved. The default seed is 20260923, and FUZZ_SEED and FUZZ_STEPS change the seed and the length. The test uses a fixed seed and is not coverage-guided.
On the build this page ships with, the contract suite ran 44 tests with 44 passing. The site's own suite (npm test at the project root: close engine, Roll format and the ABI layer checked against the compiled selectors) ran 20 with 20 passing, and npm run e2e drove the site's wallet layer through connect, approve, deposit, settlement, Roll publication, verification and cash-out against the real contracts on a local node. Run them yourself rather than taking these counts on trust.
Two more test files check the glue. One confirms that the exported ABI files match the compiled contracts, that every signature the site uses is present in the bytecode, and that rolls(uint16) keeps its output order. The other runs the operator scripts end to end on a local node: the deploy seeds the vault at exactly 1.000000 and refuses chain 4663 without CONFIRM_MAINNET=yes, add-rent is a dry run by default and rejects amounts finer than six decimals, publish-roll enforces numbering and stores the hash and price, and verify-roll prints MATCH for the published file and MISMATCH for anything else.
Checks the site makes
- Identity check. Before it shows vault figures, the site confirms that the configured vault's
asset()is USDG and itsdecimals()is 18. - Exact-amount approvals. A deposit asks for approval of the amount being deposited, never for an unlimited allowance.
- Chain and account re-checked. Before every transaction the site asks the wallet again for its chain ID and account, and stops if the chain is not 4663 or the account has changed.
- Quotes from the chain. Deposit and cash-out amounts come from
previewDepositandpreviewRedeemon the vault. - Reads through the public RPC. Balances, price and cap come from Robinhood Chain's public RPC, so the figures do not depend on what a wallet reports.
- Roll hashes checked in the browser. /roll#verify hashes the file in your browser and reads the registry itself.
Known limitations
- USDG is controlled by its issuer. It is upgradeable and pausable, and a pause would stop cash-outs along with every other USDG transfer. The tests use a mock USDG, so they do not cover the real token's behaviour.
- The operator is trusted. It reports the Roll figures, sends the net rent and holds the owner keys. A hash can prove a file is unchanged. It cannot prove the figures are true.
- The owner key can pause deposits and change the cap. Whoever holds the registry's owner key could publish a false Roll under the next number. Neither key can move USDG out of the vault.
- There has been no third-party audit.
Reporting a problem
A security contact will be published before launch. Until then, reach the project through @rentdayxyz on X.