Skip to content

Latest commit

 

History

History
265 lines (217 loc) · 11.4 KB

File metadata and controls

265 lines (217 loc) · 11.4 KB

Architecture

How cache. fits together, end to end. Read this first if you're new to the codebase — it should give you the whole topology in ~15 minutes. Deep dives live in the per-area docs linked throughout.

cache. is a DIEM yield vault. Depositors hand it DIEM; it stakes that DIEM on Venice, resells the resulting daily inference allowance on inference marketplaces, and compounds the USDC proceeds back into more DIEM. A depositor's share token (vDIEM) claims a growing slice of the underlying over time.


The pieces

Layer Lives in Tech Runs on
Vault contract contracts/ Solidity, Foundry, OpenZeppelin Base mainnet
Off-chain scripts scripts/ TypeScript, viem Railway (one long-running Node process)
Web UI web/ Next.js (App Router), wagmi, RainbowKit, viem Vercel (static/edge)
Ownership Safe multisig Base mainnet
flowchart TB
  subgraph Base["Base mainnet"]
    Vault["DiemVault.sol\n(ERC-4626 + async redeem + ERC-1271)"]
    Diem["Venice DIEM\n(stake / cooldown / unstake)"]
    Aero["Aerodrome SlipStream\n(USDC/DIEM pool)"]
    Safe["Safe multisig\n(vault owner)"]
  end
  subgraph Railway["Railway (Node process)"]
    Cron["cron.ts scheduler"]
    Harvest["harvest.ts"]
    Pricer["pricer.ts"]
    Register["register-seller.ts"]
  end
  subgraph Vercel["Vercel"]
    UI["Next.js UI\n(/, /contracts, /dashboard, /manifesto)"]
  end
  Surplus["Surplus Intelligence\n(inference marketplace)"]
  Venice["Venice API\n(inference)"]
  User["Depositor"]

  User -->|deposit DIEM / redeem| UI
  UI -->|reads + wallet txns| Vault
  Vault <-->|stake / unstake| Diem
  Vault <-->|swap USDC→DIEM| Aero
  Safe -->|admin: setKeeper / setAuthorizedSigner / setDepositCap| Vault
  Cron --> Harvest --> Vault
  Cron --> Pricer --> Surplus
  Cron --> Register --> Surplus
  Register -.->|payout_address = Vault| Surplus
  Surplus -->|inference requests| Venice
  Surplus -->|settle USDC| Vault
Loading

1. The vault contract (contracts/src/DiemVault.sol)

A single contract, DiemVault, extending OpenZeppelin ERC4626, Ownable, ReentrancyGuard, and implementing IERC1271.

ERC-4626 core. Deposits are standard and synchronous: deposit DIEM, the vault immediately stakes it on Venice's DIEM contract, and mints vDIEM shares. The share token uses a _decimalsOffset() of 6 (DIEM is 18dp ⇒ vDIEM is 24dp) as OZ's defence against the first-depositor donation/inflation attack. The human-readable share rate at a fresh vault is exactly 1.0 — the 10^6 offset cancels on both sides of every conversion. (See web/lib/format.ts for the full decimal explainer and the foot-gun it guards against.)

Async redemption. Withdrawals are not synchronous, because Venice's DIEM contract permits only one cooldown per holder. So redemptions are batched:

sequenceDiagram
  participant U as User
  participant V as DiemVault
  participant D as Venice DIEM
  U->>V: requestRedeem(shares)
  Note over V: shares burned, assets reserved in the open batch
  Note over V,U: ≥ minBatchOpenSecs later (default 1 day)
  U->>V: flush()  (permissionless)
  V->>D: initiateUnstake(batchTotal)
  Note over D: Venice cooldown begins (~24h)
  Note over V,U: after cooldown elapses
  U->>V: unstakeBatch(batchId)  (permissionless)
  V->>D: unstake()  → DIEM returns to vault
  U->>V: claimRedeem(batchId)
  V->>U: DIEM transferred
Loading

Worst-case request→claim is ~48h (24h batch window + 24h cooldown); average ~36h. flush(), unstakeBatch(), and claimRedeem() are permissionless — anyone can poke the lifecycle forward — but claimRedeem only pays out to the batch's own depositor (msg.sender-keyed accounting).

DIEM accounting. Gross DIEM under custody is split across three buckets: totalStaked (staked on Venice), cooldownAmount (post-flush, pre-unstake), and drainedClaimable (returned, earmarked for claims). totalAssets() derives from these; donations that land in balanceOf are inert.

Harvest. harvest(amountIn) swaps accumulated USDC (from inference sales, settled directly to the vault) into DIEM via the Aerodrome SlipStream router, re-stakes it, and so raises the share rate. Slippage is bounded on-chain (maxHarvestSlippageBps, default 1%, hard-ceiled at 5%) using the SlipStream quoter. harvest() is keeper-only for now: the spot quoter can't defend against pre-quote pool manipulation (a flashloan sandwich), so it stays permissioned until TWAP protection lands (see Tier 2 / the trustless-keeper work). The owner Safe is always also permitted as a fallback.

ERC-1271. The vault implements isValidSignature(hash, sig) and returns the magic value iff sig came from authorizedSigner. This is how the vault authenticates to Venice's headless API-key issuance flow — the operator signs a challenge as the vault without the vault ever exposing custody.

See contracts/SECURITY.md for the full trust model and the emergency-response operating model (there is no pause(), by design).


2. Off-chain scripts (scripts/)

TypeScript + viem. One long-running Node process on Railway; cron.ts is the entry point and schedules the rest. All three jobs are also runnable one-shot locally (npm run harvest / register-seller / pricer).

flowchart LR
  Cron["cron.ts\n(scheduler)"]
  Cron -->|*/30 min| H["harvest.ts"]
  Cron -->|hourly :07| P["pricer.ts"]
  Cron -->|Sun 08:00| R["register-seller.ts"]
  H -->|vault.harvest()| Vault[(DiemVault)]
  P -->|PATCH prices| Surplus[(Surplus API)]
  R -->|SIWE + list offers| Surplus
Loading
  • harvest.ts — quotes Aerodrome SlipStream off-chain, applies a slippage floor, and calls vault.harvest() when the vault's USDC balance clears HARVEST_MIN_USDC. Signs as the keeper EOA.
  • register-seller.ts — SIWE-authenticates to Surplus as the seller and reconciles one active offer per Venice model, with payout_address set to the vault. Idempotent. Does not touch price (the pricer owns price).
  • pricer.ts — the dynamic-pricing loop. Reads Surplus's public markets feed, computes a target price that undercuts the cheapest healthy competitor (floored at a fraction of the reference price), and PATCHes per-model prices. Pricer is the sole authority on price so it can't fight the reconciler.
  • lib/env.ts (env validation), viem.ts (Base clients), surplus.ts (Surplus API client).

The operator EOA configured here (OPERATOR_PRIVATE_KEY) is the vault's keeper and authorizedSigner. It holds no vault custody — its powers are bounded to harvest() (1% slippage grief ceiling) and signing Venice challenges. See scripts/README.md for the full env list, schedules, and tuning knobs.


3. The web UI (web/)

Next.js App Router, statically rendered, client-side on-chain reads only. No server, no database, no secrets — every number is read from the chain in the user's browser via wagmi/viem; wallet connectivity is RainbowKit + WalletConnect.

Routes:

  • / — the vault console: stats, deposit/redeem actions, withdrawal queue, harvest history.
  • /contracts — every official Base address with Basescan/Safe deep-links.
  • /dashboard — read-only on-chain activity: harvests, deposits, redemption-lifecycle events, "days since last harvest", and a reconstructed TVL-over-time chart (useVaultActivity in lib/hooks.ts).
  • /manifesto — the thesis.

Key modules: lib/wagmi.ts (chain config + canonical addresses), lib/abi.ts (hand-maintained minimal ABIs), lib/hooks.ts (all on-chain reads), lib/format.ts (token-decimal formatting + the vDIEM explainer). Baseline security headers are set in next.config.ts; see web/README.md.


4. Deployment topology

flowchart TB
  subgraph BaseChain["Base mainnet"]
    V[(DiemVault)]
    S[(Safe multisig)]
  end
  Vercel["Vercel\n(web/ — static + edge)"] -->|RPC reads / wallet txns| V
  Railway["Railway\n(scripts/ — 1 Node process)"] -->|keeper txns| V
  Railway -->|HTTPS| SurplusAPI[(Surplus API)]
  Operator["Operator EOA\n(keeper + authorizedSigner)"] -.->|key in Railway env| Railway
  HW["Hardware wallet"] -.->|signs| S
  S -->|owns / admin| V
Loading
  • Vault + Safe: Base mainnet. The Safe owns the vault; admin calls (setKeeper, setAuthorizedSigner, setDepositCap, setMinBatchOpenSecs, setMaxHarvestSlippageBps) are Safe-only.
  • UI: Vercel, static. Public env (NEXT_PUBLIC_*) only.
  • Scripts: Railway, a single always-on Node process. Holds the operator key (OPERATOR_PRIVATE_KEY) encrypted at rest. If Railway is compromised, the Safe rotates keeper/authorizedSigner and the operator is redeployed fresh — no depositor funds are ever at risk from the operator key.

See LAUNCH_RUNBOOK.md for the full deploy procedure.


5. End-to-end data flow

The money path, one full cycle:

flowchart LR
  A["User deposits DIEM"] --> B["Vault stakes on Venice"]
  B --> C["Daily inference allowance accrues"]
  C --> D["register-seller lists Venice models on Surplus\n(payout = vault)"]
  D --> E["Buyers consume inference via our API key"]
  E --> F["Surplus settles USDC → vault"]
  F --> G["harvest.ts: swap USDC → DIEM on Aerodrome, re-stake"]
  G --> H["Share rate (vDIEM:DIEM) rises"]
  H --> A
Loading
  1. Deposit → stake. User deposits DIEM; the vault stakes it on Venice and mints vDIEM.
  2. Sell. register-seller.ts lists the vault's Venice models on Surplus with the vault as payout address; pricer.ts keeps them competitively priced.
  3. Settle. Buyers consume inference (authenticated by the vault via ERC-1271); Surplus settles USDC directly to the vault.
  4. Harvest → reinvest. harvest.ts swaps USDC → DIEM on Aerodrome and re-stakes, raising the share rate.
  5. Redeem. When a user wants out, the async requestRedeem → flush → unstakeBatch → claimRedeem lifecycle returns their DIEM.

6. Auth model

Who can do what:

Actor Holds Can Cannot
Safe (owner) multisig keys (hardware-wallet signers) setKeeper, setAuthorizedSigner, setDepositCap, setMinBatchOpenSecs, setMaxHarvestSlippageBps withdraw/seize depositor funds (no such function)
Keeper (operator EOA) OPERATOR_PRIVATE_KEY on Railway call harvest() (≤ slippage ceiling) move custody; unbounded loss
Authorized signer (same EOA) same key sign Venice ERC-1271 challenges as the vault spend vault assets
Depositors their own wallet deposit, requestRedeem, claimRedeem (their own batch); permissionlessly flush/unstakeBatch touch anyone else's position
Anyone flush(), unstakeBatch() (poke the lifecycle forward) claim funds that aren't theirs

Soft-pause levers (no hard pause() by design): the Safe can setDepositCap(0) to halt new deposits and setKeeper(0x0) to halt harvest. Redemptions remain permissionlessly pokeable so users can always exit. Full detail in contracts/SECURITY.md.


Where to go next