Documentation
Everything Robills does, written down.
The rate source, the arithmetic, the contract, the custody path and what can go wrong. If something here contradicts the interface, trust the contract.
Introduction
Overview
Robills is a fixed-term vault. You deposit ETH for a chosen term; the vault records the U.S. Treasury bill yield live at that moment and owes you principal plus that yield when the term ends. Behind the scenes the capital is converted and used to buy actual Treasury bills of matching maturity, held by a KYC-verified treasury operator.
There is no lending pool, no leverage, no rehypothecation and no other protocol in the dependency chain. The only counterparty is the treasury operator, and the only instrument is a sovereign bill.
What Robills is not
Introduction
Position lifecycle
Swipe the table to see every column →
| Stage | Who acts | What happens |
|---|---|---|
| Quote | Rate engine | Reads the Treasury daily bill curve, subtracts the platform spread, signs an EIP-712 quote valid for 5 minutes. |
| Deposit | You | Send ETH with the signed quote. The vault verifies the signature and writes principal, APY and maturity into storage. |
| Sweep | Operator | Moves unreserved ETH to the treasury account. Cannot touch ETH already reserved for settled positions. |
| Deploy | Operator | Converts to USD and buys Treasury bills with maturities matching the tenor. |
| Fund | Operator | On redemption, returns proceeds to the vault via fundSettlement(). |
| Settle | Operator | Marks matured positions claimable. Reverts unless the vault already holds every payout in the batch. |
| Claim | You | Permissionless withdrawal of principal plus yield. The operator cannot reverse it. |
Introduction
Tenors & limits
Four tenors, each mapped to the Treasury bill that matures closest to it. Limits are enforced by the contract, not just the interface.
Swipe the table to see every column →
| Tenor | Days | Benchmark | Spread | Min / Max |
|---|---|---|---|---|
| 1 Month | 30 | 4-Week Bill | 35 bp | 0.05 – 250 ETH |
| 3 Months | 91 | 13-Week Bill | 35 bp | 0.05 – 500 ETH |
| 6 Months | 182 | 26-Week Bill | 30 bp | 0.10 – 1,000 ETH |
| 12 Months | 364 | 52-Week Bill | 25 bp | 0.10 – 2,000 ETH |
Day counts are the settlement days used by the yield formula, not calendar months. A “3 month” position runs 91 days from the block your deposit confirmed in.
Mechanics
Rate mechanics
Source
The backend polls the U.S. Treasury's Daily Treasury Bill Rates publication and reads the coupon-equivalent yield — the investment-basis figure, not the bank discount rate. The CSV export is primary; the XML feed is the fallback. Both are parsed for the most recent row that has every tenor populated.
Spread
Robills withholds a fixed spread per tenor, shown on every quote and every rate card. It covers execution, custody and operations. Nothing else is deducted — there is no performance fee, no exit fee and no management fee on top.
netBps = grossCouponEquivalentBps − spreadBps 12M example (11 Sep 2026): gross 4.34% (52-week bill, coupon equivalent) spread 0.25% net 4.09% ← what your position earns
Locking
A quote is signed with a 5-minute expiry and a single-use nonce. When your deposit lands, apyBps is copied into the position struct. From that block on, the payout is fixed arithmetic — a later curve move, a spread change, or even the operator reconfiguring the tenor cannot alter it.
If the feed goes dark
RATES_MAX_STALE_HOURS. Existing positions are unaffected; only new deposits pause.Mechanics
Yield formula
Simple accrual on an ACT/365 basis — the same convention the Treasury uses for the coupon-equivalent figure. No compounding, because a bill does not compound.
payout = principal × (1 + apyBps / 10 000 × days / 365) 10 ETH, 12M tenor, 4.09% locked: 10 × (1 + 0.0409 × 364 / 365) = 10.407879452054794520 ETH yield = 0.407879452054794520 ETH
The contract computes this in wei with integer arithmetic, so the value it stores is authoritative and the interface merely mirrors it. Truncation is at most 1 wei and always in the vault's favour.
Mid-term value
The app shows linear accrual between deposit and maturity. That is presentational: there is no early redemption path, so accrued value is not withdrawable before maturity.
Mechanics
Settlement & claims
After maturity the operator funds the vault and calls settle(ids). That call reverts unless the vault's free balance already covers every payout in the batch, so a position can never be marked claimable against money that is not there.
Settled payouts move into settlementReserve. That balance is excluded from sweepableBalance(), which means the operator cannot move it out for any reason. Only your own claim reduces it.
Shortfall path
If bill proceeds do not arrive in full, the operator can call refund(ids), which makes the positions claimable at principal only and forfeits the yield. It still requires the ETH to be present. There is no path that makes a position claimable for less than principal.
Protocol
Network
Robills runs on Robinhood Chain — an Arbitrum Orbit L2 that posts its data to Ethereum. It is fully EVM compatible, so any standard wallet connects without modification.
Robinhood Chain has no native chain token: gas is paid in ETH, the same asset you deposit. That is convenient here — one asset for the position and for the fee to claim it.
Swipe the table to see every column →
| Mainnet | Testnet | |
|---|---|---|
| Chain ID | 4663 | 46630 |
| RPC | rpc.mainnet.chain.robinhood.com | rpc.testnet.chain.robinhood.com |
| Explorer | robinhoodchain.blockscout.com | explorer.testnet.chain.robinhood.com |
| Gas token | ETH | ETH (test) |
If your wallet is on the wrong network, the app shows a Switch network button and will add Robinhood Chain for you if it is not already configured.
Positions are chain-local
Protocol
RobillsVault
Single contract, Solidity 0.8.24, OpenZeppelin 5 for access control, pausing, reentrancy and EIP-712. Not upgradeable — a new version means a new deployment, and existing positions run out on the contract they were opened in.
Key entry points
Swipe the table to see every column →
| Function | Caller | Notes |
|---|---|---|
deposit(Quote, bytes) | Anyone | Payable. Verifies the signed quote, opens a position. |
claim(uint256) | Position owner | Withdraws a settled position. Works even while paused. |
claimAll() | Position owner | Claims every settled position the caller owns. |
sweepToTreasury(uint256) | Owner | Moves unreserved ETH only. Pass 0 for the full amount. |
fundSettlement() | Anyone | Payable. Returns bill proceeds to the vault. |
settle(uint256[]) | Owner | Marks matured positions claimable. Balance-checked. |
refund(uint256[]) | Owner | Shortfall path — principal only, yield forfeited. |
pause() / unpause() | Owner | Stops new deposits. Claims and settlement stay open. |
Quote type
domain = { name: "Robills", version: "1", chainId, verifyingContract }
Quote(
address account, // the only wallet that may use this quote
uint8 tenor, // 0=1M 1=3M 2=6M 3=12M
uint32 apyBps, // net rate, basis points
uint256 maxPrincipal, // ceiling on msg.value
uint64 deadline, // unix seconds
bytes32 nonce // single use
)Position storage
address owner; uint8 tenor; uint32 apyBps; // frozen at deposit uint64 openedAt; uint64 maturesAt; // openedAt + durationDays uint128 principal; uint128 payout; // frozen at deposit Status status; // Active | Settled | Claimed
Protocol
Security model
The invariant
address(this).balance >= settlementReserve
settlementReserve is the sum of payouts on settled, unclaimed positions. Sweeping subtracts it before computing what is movable; settling adds to it only after checking the balance exists. Nothing but a claim reduces it.
What the operator can do
- Sweep unreserved ETH to the treasury address.
- Pause new deposits.
- Reconfigure tenors — for future deposits only.
- Rotate the treasury address and the quote signer.
What the operator cannot do
- Touch ETH reserved for a settled position.
- Change the rate, principal or maturity of a live position.
- Claim on your behalf or redirect your payout.
- Mark a position claimable without the ETH being present.
Signer compromise
A stolen quote-signer key could mint positions at an inflated rate — so every tenor carries a maxApyBps ceiling (default 20.00%) that the contract enforces independently. The owner can rotate the signer in one transaction. The signer key never has custody of any funds.
Audit status
Protocol
Custody & KYC
Swept capital is held by a KYC-verified operator in a brokerage account that can purchase U.S. Treasury bills directly. The operator's identity is verified; the account is not an anonymous multisig.
The window between sweep and settlement is the part of Robills that is not enforced by code. During that window the obligation to return proceeds is contractual, not cryptographic. That is the honest shape of any real-world-asset product, and it is the main thing to weigh before depositing.
fundingGap() reports, at any moment, how much ETH the vault would still need to cover every outstanding obligation. It is public and callable by anyone.
Integrate
API reference
Base URL: http://localhost:4000 in development. JSON in, JSON out. Rate limited to 240 requests per minute per IP. No authentication on read endpoints.
/healthRATES_MAX_STALE_HOURS./api/v1/rates/curve/api/v1/rates/tenors/api/v1/rates/eth/api/v1/rates/refresh/api/v1/quotes{ tenorId, principalEth, account? }. With an account and a configured signer key, the response includes an onchain object you can pass straight to deposit()./api/v1/positions/:address/api/v1/positions/api/v1/positions/:id/api/v1/statsQuote response
{
"quoteId": "5769ab49-…",
"tenorId": "T12M",
"principalEth": 5,
"grossBps": 434,
"spreadBps": 25,
"netBps": 409,
"days": 364,
"yieldEth": 0.20393972602739727,
"payoutEth": 5.203939726027397,
"maturityAt": "2027-09-13T19:59:17.836Z",
"expiresAt": "2026-09-14T20:04:17.836Z",
"onchain": {
"tenor": 3,
"apyBps": 409,
"maxPrincipal": "5000000000000000000",
"deadline": 1789416257,
"nonce": "0xa19b95aa…",
"signature": "0x…"
}
}Integrate
Running locally
cd backend cp .env.example .env npm install npm run seed # optional demo positions npm run dev # http://localhost:4000
cd frontend cp .env.local.example .env.local npm install npm run dev # http://localhost:3000
cd contracts cp .env.example .env npm install npx hardhat test npx hardhat node # terminal 1 npm run deploy:local # terminal 2
Storage defaults to a single JSON file, so there is no database to run. Set STORE_DRIVER=mysql and apply schema.sql when you want MySQL instead — the adapter is a drop-in.
Offline compiles
binaries.soliditylang.org, run Hardhat with SOLC_LOCAL=true and it will use the solc package from npm instead.Integrate
Deploying
Order matters — the three pieces reference each other.
- Generate a dedicated quote-signer wallet. It holds no funds. Put its private key in the backend's
QUOTE_SIGNER_KEY. - Set
TREASURY_ADDRESSandQUOTE_SIGNER_ADDRESS, thennpm run deploy:testnet(chain 46630) and, once you have run a full deposit → settle → claim cycle there,npm run deploy:mainnet(chain 4663). - Copy the deployed address into the backend's
VAULT_ADDRESSand the frontend'sNEXT_PUBLIC_VAULT_ADDRESS, and setCHAIN_IDon both. - Restart the backend so the EIP-712 domain matches the deployment. A mismatched domain makes every signature invalid.
- Set
ALLOW_STALE_QUOTES=falseandNODE_ENV=production.
Domain separator
chainId and verifyingContract are part of the EIP-712 domain. A testnet deployment and a mainnet one therefore need separate backend configurations — quotes signed for 46630 are rejected by the vault on 4663, and vice versa.Verification
Robinhood Chain uses Blockscout rather than Etherscan. The Hardhat config already carries the custom-chain entries, and Blockscout ignores the API key, so npx hardhat verify --network robinhood <address> <args> works without one. The deploy script runs it for you after six confirmations.
Legal
Risk disclosure
Operator risk
Between sweep and settlement, capital sits off-chain with the treasury operator. If the operator fails to return proceeds, the contract cannot conjure them. This is the largest single risk and it is not mitigated by code.
Currency risk
You deposit ETH and claim ETH. The bills are dollar instruments, so the operator converts on the way in and back on the way out. Your ETH-denominated return is the locked rate; your dollar-denominated return also depends on where ETH trades, and the conversion at settlement may be worse than at deposit.
Liquidity risk
There is no early exit. Capital is locked for the full tenor. Size positions on the assumption you cannot reach them until maturity.
Smart contract risk
The contract is tested but unaudited. Bugs in the vault, the signer infrastructure or the frontend could result in loss.
Regulatory risk
Offering yield derived from securities is regulated in most jurisdictions, and the rules differ by where the operator and the depositor sit. Availability may change, and operators should take their own legal advice before accepting deposits.
Not advice
Legal
FAQ
Why is the 12-month rate higher?
Because the 52-week bill yields more than the 4-week bill on the current curve. Robills passes the shape of the curve through rather than smoothing it.
Can the rate change after I deposit?
No. apyBps and payout are written into storage at deposit and nothing in the contract can rewrite them.
What happens at maturity if I do not claim?
The payout waits in the vault indefinitely. It is reserved for you and cannot be swept. It does not continue to accrue.
Do I get a position NFT?
No. Positions are non-transferable by design, tied to the depositing address. Transferability would turn the position into a tradable instrument with a very different regulatory shape.
Which address should I use?
Whichever wallet you can sign from at maturity. The claim is restricted to the depositing address, and there is no recovery path for a lost key.
Ready to open a position?
Launch the app