Architecture.
Three small contracts around one B20 token. The token holds balances and enforces compliance; the clock holds the books and moves the multiplier; the oracle prices bills; the desk lets anyone in and out.
Components
| Component | What it does | What it may change |
|---|---|---|
| LUSG (B20 Asset precompile) | Balances, transfers, the multiplier, roles, policies, pause, cap | Itself, as its roles allow |
| LusgClock | The basket's books: bill lots and cash. Values each lot at amortized cost. tick() schedules the next minute's multiplier; check() compares market and amortized value | LUSG's multiplier only (it holds OPERATOR_ROLE and nothing else) |
| BillOracle | Treasury's seven bill rates and every outstanding bill by CUSIP; priceOf(cusip) | Its own data, posted by the poster |
| LusgDesk | Buy LUSG with USDV and redeem it, at the basket's value; test KYC on Vibenet | Mints and burns LUSG; books cash in the clock; edits the KYC allowlist |
| Keeper (off chain) | Calls tick() each minute and the daily check | Nothing it can set: both calls are permissionless and compute their own values |
Who holds what
| Right | Holder on Vibenet |
|---|---|
LUSG DEFAULT_ADMIN_ROLE, METADATA_ROLE | Admin (deployer) wallet |
LUSG MINT_ROLE, BURN_ROLE | Minter wallet and LusgDesk |
LUSG PAUSE_ROLE, UNPAUSE_ROLE, SEIZE_ROLE | Compliance wallet |
LUSG OPERATOR_ROLE | LusgClock |
| Clock manager (books) | LusgDesk, which passes the admin wallet's calls through |
| KYC allowlist admin | LusgDesk (staged by compliance, accepted by the desk) |
| Sanctions blocklist admin, seize-exempt list admin | Compliance wallet |
| BillOracle poster | Keeper wallet |
The four flows
1 · Buy
wallet ── approve(desk, usd) ─────────────▶ USDV
wallet ── buy(usd, minOut) ──────────────▶ LusgDesk
│ price = clock.nav() / LUSG.totalSupply()
│ not on allowlist? registry.updateAllowlist(+wallet) ← Vibenet test KYC
│ USDV.transferFrom(wallet → desk, usd)
│ clock.depositCash(usd) ← basket value rises by usd
└ LUSG.mint(wallet, usd / price) ← supply rises in proportion
B20 checks MINT_RECEIVER policy: allowlisted and not blocklisted
2 · The minute step
keeper ── tick() ──▶ LusgClock
│ pending step still live? return false (one pending update at a time)
│ at = next whole minute
│ m = navAt(at) / totalSupply ← lots straight-line from cost to face, plus cash
└ LUSG.updateUIMultiplier(m, at) ← takes effect at `at`, no event at the flip
3 · The daily check
Treasury CSV ──▶ keeper ── postRates(asOf, 7 rates) ──▶ BillOracle
anyone ── check() ──▶ LusgClock ── priceOf(cusip) per lot ──▶ BillOracle
└ gap = market / amortized − 1; |gap| > 0.5% → alerting = true
4 · Redeem
wallet ── approve(desk, lusg) ───────────▶ LUSG
wallet ── redeem(lusg, minOut) ──────────▶ LusgDesk
│ usdOut = lusg × price; usdOut ≤ desk's USDV?
│ LUSG.transferFrom(wallet → desk) ← sender must not be blocklisted
│ LUSG.burn(lusg)
│ clock.withdrawCash(usdOut)
└ USDV.transfer(wallet, usdOut)
Design rules the code keeps
- Absolute, not cumulative. Every multiplier is the basket's value over supply, so a missed minute only holds the value still, and the next tick catches up.
- A keeper can delay, never decide.
tick()andcheck()are permissionless and compute their own values from the lots and the oracle. - Trades don't move the price. The desk books cash before minting and after burning, so value per token is unchanged (tested to rounding dust).
- Least privilege. The clock can change the multiplier and nothing else. The desk can mint, burn and grant test KYC, but can't seize, pause or change policies.