Contracts
Two small contracts: a vault that holds USDG and a registry that records each Roll.
Overview
Rentday runs on two Solidity contracts, both built on OpenZeppelin Contracts 5 and compiled with Solidity 0.8.28. The source is in contracts/src, the exported ABIs in contracts/abi and the tests in contracts/test. contracts/README.md documents the package.
Both contracts are live on Robinhood Chain, deployed through the operator console. Their addresses are on Chain and addresses. Neither contract is upgradeable, and the deployed runtime bytecode of both matches the compiled artifacts, with immutables masked for the comparison.
RentdayVault
An OpenZeppelin ERC-4626 vault over USDG. The share token is named Rentday Vault Share, with the symbol vRENT and 18 decimals.
| Feature | Behaviour |
|---|---|
| Constructor | constructor(usdg, initialOwner, initialCap). Reads the asset's decimals() and reverts with RentdayVaultInvalidAssetDecimals unless it is 6. Emits CapUpdated(0, initialCap). The live vault was deployed with a cap of 100,000 USDG. |
totalAssets() | The vault's USDG balance, the OpenZeppelin default. Nobody pushes a valuation in. |
| Decimals offset | _decimalsOffset() returns 12, so shares have 6 + 12 = 18 decimals and 10^12 virtual shares stand against one virtual unit of USDG. An empty vault therefore prices one vRENT at exactly 1.000000 USDG without a seed deposit. Income arrives as plain transfers, so donations to the vault are possible by design, and the offset makes a first-depositor or donation inflation attack cost the attacker far more than it can extract. |
| Deposit cap | cap() returns the cap in USDG base units, compared with totalAssets(). setCap(uint256) is owner-only and emits CapUpdated(previousCap, newCap). |
| Pause | pause() and unpause() are owner-only. While paused, maxDeposit and maxMint return 0, so deposit and mint revert with ERC4626ExceededMaxDeposit and ERC4626ExceededMaxMint. redeem, withdraw, share transfers and addCapital keep working. |
addCapital(uint256 amount) | Callable by anyone. Pulls amount USDG from the caller, which needs a USDG approval first, and emits CapitalAdded(from, amount). Reverts with RentdayVaultZeroAmount for 0, and with RentdayVaultNoShares while no vRENT exists, because USDG added to an empty vault would be captured by the virtual shares. The operator settles each month's fees through this function. A plain USDG transfer has the same effect on the price without the event. |
| Withdrawal paths | None for the owner. USDG leaves only through redeem or withdraw, called by a holder or by someone the holder approved. |
| Ownership | Ownable2Step: the owner calls transferOwnership(address) and the new owner calls acceptOwnership(). renounceOwnership() always reverts with RentdayVaultRenounceDisabled. |
maxMint(address) | Returns the uint256 maximum instead of reverting if the cap is set so high that the conversion would overflow, as ERC-4626 requires of its max functions. |
The vault does not know where its USDG comes from. It has no reference to $RENT, Pons or ETH, and treats every USDG added the same way.
RollRegistry
An append-only record of Rolls, numbered from 1. Each entry is stored as:
struct Roll {
bytes32 docHash; // SHA-256 of the exact CSV bytes
uint256 navPerShare; // USDG per vRENT, 12 decimals
string uri; // where the file is published
uint64 at; // block timestamp of publication
}
| Function or event | Behaviour |
|---|---|
rolls(uint16 n) returns (bytes32 docHash, uint256 navPerShare, string uri, uint64 at) | Returns Roll n in that order. An unpublished number returns zeros, with at equal to 0. |
latest() returns (uint16) | The number of the most recent Roll, or 0 before the first. |
publish(uint16 n, bytes32 docHash, uint256 navPerShare, string uri) | Owner only. n must equal latest() + 1, or the call reverts with RollRegistryUnexpectedRollNumber(expected, provided). A zero docHash reverts with RollRegistryZeroDocHash. Stores the Roll with the block timestamp and emits RollPublished. |
event RollPublished(uint16 indexed n, bytes32 docHash, uint256 navPerShare, string uri, uint64 at) | Emitted once per Roll. |
The registry has no function to change or delete a Roll. Ownership is Ownable2Step, and renounceOwnership() reverts with RollRegistryRenounceDisabled, since a registry without an owner could never publish again. Roll numbers are uint16, which allows up to 65,535 Rolls.
The registry stores whatever navPerShare the owner passes. The operator console reads it from the vault at the moment of publication, as convertToAssets(1e18) × 1,000,000.
Functions the site calls
The site encodes its calls itself, without a library, in site/js/abi.js. Its SIG table holds these signatures. Each selector is the first four bytes of the keccak-256 hash of the signature, as listed in contracts/abi/selectors.json.
| Signature | Selector | Contract | Used for |
|---|---|---|---|
balanceOf(address) | 0x70a08231 | USDG, RentdayVault | Your USDG and vRENT balances |
allowance(address,address) | 0xdd62ed3e | USDG | Your current approval to the vault |
approve(address,uint256) | 0x095ea7b3 | USDG | Exact-amount approval before a deposit or a settlement |
decimals() | 0x313ce567 | RentdayVault | Identity check (must be 18) |
symbol() | 0x95d89b41 | RentdayVault | Identity check |
asset() | 0x38d52e0f | RentdayVault | Identity check (must be USDG) |
totalAssets() | 0x01e1d114 | RentdayVault | USDG held |
totalSupply() | 0x18160ddd | RentdayVault | vRENT outstanding |
convertToAssets(uint256) | 0x07a2d13a | RentdayVault | Share price, called with 1e18 |
previewDeposit(uint256) | 0xef8b30f7 | RentdayVault | Deposit quote |
previewRedeem(uint256) | 0x4cdad506 | RentdayVault | Cash-out quote |
maxDeposit(address) | 0x402d267d | RentdayVault | Room under the cap, 0 while paused |
deposit(uint256,address) | 0x6e553f65 | RentdayVault | Deposit |
redeem(uint256,address,address) | 0xba087652 | RentdayVault | Cash out |
cap() | 0x355274ea | RentdayVault | Deposit cap |
paused() | 0x5c975abb | RentdayVault | Pause flag |
addCapital(uint256) | 0xbea10f96 | RentdayVault | Settling the month's fees |
rolls(uint16) | 0x514f5ab2 | RollRegistry | Roll verification |
latest() | 0x52bfe789 | RollRegistry | Latest Roll number |
The operator console also calls publish(uint16,bytes32,uint256,string), selector 0x41c50a25, along with the owner switches pause(), unpause() and setCap(uint256). A test in the contracts package checks that every selector in selectors.json is present in the compiled bytecode.
Source and tests
cd contracts
npm install
npm test
The suite runs 44 tests, all passing. Security describes what they cover, and Operator guide shows the contracts in monthly use.