RobillsRobills

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.

v1.0.0Solidity 0.8.24Robinhood Chain · 4663

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

It is not a money-market fund, not a security, not a bank deposit and not insured. It is software plus an operating agreement. Read Risk disclosure before depositing.

Introduction

Position lifecycle

Swipe the table to see every column →

StageWho actsWhat happens
QuoteRate engineReads the Treasury daily bill curve, subtracts the platform spread, signs an EIP-712 quote valid for 5 minutes.
DepositYouSend ETH with the signed quote. The vault verifies the signature and writes principal, APY and maturity into storage.
SweepOperatorMoves unreserved ETH to the treasury account. Cannot touch ETH already reserved for settled positions.
DeployOperatorConverts to USD and buys Treasury bills with maturities matching the tenor.
FundOperatorOn redemption, returns proceeds to the vault via fundSettlement().
SettleOperatorMarks matured positions claimable. Reverts unless the vault already holds every payout in the batch.
ClaimYouPermissionless 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 →

TenorDaysBenchmarkSpreadMin / Max
1 Month304-Week Bill35 bp0.05 – 250 ETH
3 Months9113-Week Bill35 bp0.05 – 500 ETH
6 Months18226-Week Bill30 bp0.10 – 1,000 ETH
12 Months36452-Week Bill25 bp0.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.

net rate
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

In production the backend stops issuing quotes once the cached curve exceeds 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
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 →

MainnetTestnet
Chain ID466346630
RPCrpc.mainnet.chain.robinhood.comrpc.testnet.chain.robinhood.com
Explorerrobinhoodchain.blockscout.comexplorer.testnet.chain.robinhood.com
Gas tokenETHETH (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

A position lives on the chain it was opened on and is claimed there. Testnet positions are not portable to mainnet, and the vault rejects quotes signed for a different chain ID.

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 →

FunctionCallerNotes
deposit(Quote, bytes)AnyonePayable. Verifies the signed quote, opens a position.
claim(uint256)Position ownerWithdraws a settled position. Works even while paused.
claimAll()Position ownerClaims every settled position the caller owns.
sweepToTreasury(uint256)OwnerMoves unreserved ETH only. Pass 0 for the full amount.
fundSettlement()AnyonePayable. Returns bill proceeds to the vault.
settle(uint256[])OwnerMarks matured positions claimable. Balance-checked.
refund(uint256[])OwnerShortfall path — principal only, yield forfeited.
pause() / unpause()OwnerStops new deposits. Claims and settlement stay open.

Quote type

EIP-712
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

struct Position
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

The contract ships with a 35-case test suite covering the deposit, settlement and claim paths. It has not been through a third-party audit. Treat any mainnet deployment accordingly.

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.

GET/health
Liveness plus curve freshness. Returns 503 when the curve is older than RATES_MAX_STALE_HOURS.
GET/api/v1/rates/curve
The live net curve: gross yield, spread, net APY, day count and change versus the prior close for all four tenors.
GET/api/v1/rates/tenors
Static tenor metadata — day counts, deposit limits, benchmark names.
GET/api/v1/rates/eth
ETH/USD spot for display only. Never used in yield math.
POST/api/v1/rates/refresh
Forces a pull from the Treasury feed. Returns 502 if every source fails.
POST/api/v1/quotes
Body: { tenorId, principalEth, account? }. With an account and a configured signer key, the response includes an onchain object you can pass straight to deposit().
GET/api/v1/positions/:address
A wallet's positions plus a summary of locked principal, pending yield and claimable balance.
POST/api/v1/positions
Mirrors a confirmed deposit so the UI can list it without an archive node. The chain stays the source of truth.
PATCH/api/v1/positions/:id
Updates status after a claim confirms.
GET/api/v1/stats
Protocol aggregates: TVL, active positions, weighted APY.

Quote response

POST /api/v1/quotes
{
  "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

backend
cd backend
cp .env.example .env
npm install
npm run seed      # optional demo positions
npm run dev       # http://localhost:4000
frontend
cd frontend
cp .env.local.example .env.local
npm install
npm run dev       # http://localhost:3000
contracts
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

If your network blocks 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_ADDRESS and QUOTE_SIGNER_ADDRESS, then npm 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_ADDRESS and the frontend's NEXT_PUBLIC_VAULT_ADDRESS, and set CHAIN_ID on both.
  • Restart the backend so the EIP-712 domain matches the deployment. A mismatched domain makes every signature invalid.
  • Set ALLOW_STALE_QUOTES=false and NODE_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

Nothing here is investment, legal or tax advice. Historical and indicative yields are not guarantees. You can lose money.

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