01 Contract status
Feature-complete pre-release, verified end-to-end on anvil
Everything in the core product works today against a local deployment: pools, swaps, lending, and all three atomic flash flows. What remains before mainnet is hardening, not construction.
- Working now: Star & Delta pools, swaps, proportional + exact-LP liquidity, Star zaps, flash-leverage / flash-deleverage / zero-capital flash-liquidation, free flash loans (both V4-style on the AMM and Morpho-style on lending), adaptive-curve IRM, per-market LLTV, protocol fees, Permit2, EIP-712 auth, deadlines, and a read-only
Lensfor every UI read. - CI: two-tier fuzzing — moderate on PRs, deep on
main. Foundry pinned to v1.7.1. - Manipulation economics de-risked: the one candidate atomic-arbitrage path against the oracle-free model (tilt-to-liquidate) was worked through in marimo notebook experiments and locked in by mirrored Foundry suites — it doesn't pay (details in §05).
- Honest caveats open: no external audit yet; symbolic/formal verification (Halmos-era suite) was retired and not yet replaced; no incentives layer; Router is ERC20-only (native ETH must be wrapped).
02 Architecture
Two contracts, cleanly split — satellites plug in
Unimod — AMM
Singleton, flash accounting (unlock → deltas → settle), ERC6909 LP shares. n-asset Star pools (LP-as-numeraire, ≤16 assets) and Delta pools (pairwise price matrix, ≤8). Knows nothing about lending.
UnimodLending
Morpho-Blue-style isolated / permissioned markets. Own ERC20 reserves; collateral is Unimod LP shares pulled over ERC6909. No flash accounting — plain transfers, checks-effects-interactions.
StarSpm / DeltaSpm · Irm
Pluggable pricing models own the whole pricing pipeline per pool type; adaptive-curve IRM sets per-market rates. Both swappable by governance without touching the core.
Router · Lens · Permit2
Router is the single user entry point and implements every callback (unlock, borrow, liquidate, withdraw-collateral). Lens answers every read/preview off-chain. Permit2 is the only ERC20 pull surface.
The split is deliberate: the AMM stays reviewable on its own, and lending failure modes cannot reach into swap accounting. The two only meet through public surfaces (LP share transfers, pool balance reads).
Who calls what
every action enters through the Router · solid = direct call · dashed = callback · from web3/ui-flows.md
Deliberately piggy-backing on audited code
The reference implementations are vendored in reference/ and mirrored closely on purpose: the audit surface shrinks to our deltas from code that has already survived review, rather than a from-scratch codebase.
- Uniswap V4 — the AMM chassis: singleton, unlock/settle flash accounting, ERC6909 claims,
Extsload/Exttload— vendored from thePoolManagerlineage. The novel part (the SPMs) is isolated behind an interface. - Morpho Blue — the lending core is a faithful mirror: isolated markets, virtual-shares math, the authorization model (incl. EIP-712 sigs), the state-first callback pattern, the adaptive-curve IRM, the liquidation-incentive formula, and the free
flashLoan. Its three audit reports (OpenZeppelin + two Cantina) ship inaudits/as the baseline for everything mirrored. - Morpho Midnight — Morpho's newer fixed-maturity, multi-collateral credit market. From it we adopted the Recovery Close Factor (restore-to-health liquidation sizing — the exact piece that de-fangs tilt-to-liquidate, §05), softened from Midnight's revert to a clamp; we replaced its oracle-based seize↔repaid conversion with the in-kind split; and its bundler's multi-mode Permit2 pull is the template for the future
SignatureTransferpath.
03 Users & UX paths
Four roles, three signatures to onboard, one click per strategy
The product decision of the last cycle: every multi-step DeFi maneuver a user actually wants — leverage up, unwind, liquidate — is one Router transaction, with the setup cost front-loaded into three one-time approvals.
| Role | What they do | Their path |
|---|---|---|
| Swapper | Trades between pool assets, pays the swap fee | swapExactIn / swapExactOut — quotes via Lens.previewSwap* |
| Liquidity provider | Supplies pool assets, earns swap fees, receives ERC6909 LP shares | addLiquidity (proportional), zapIn/zapOut on Star pools |
| Lender | Supplies ERC20s to isolated markets, earns borrow interest | supply / withdraw (+ shares-mode variants) |
| Borrower / leverager | Posts LP shares as collateral, borrows per-asset | depositCollateral, borrow — or flashLeverage in one tx (≈4× max, balanced basket) |
| Liquidator | Closes unhealthy positions for a ~2% bonus | liquidate — zero capital, zero approvals needed |
Onboarding — three one-time approvals, then never again
- Permit2 —
token.approve(permit2)+permit2.approve(token, router, …). One approval surface covers every token; the Router never reads token allowances itself. - LP shares — ERC6909
setOperator(router), for posting collateral. - Lending authorization —
setAuthorization(router), or signed EIP-712 and bundled viamulticallwith the first lending action, so onboarding costs zero extra transactions.
- Every mutating call carries a deadline; the UI defaults to now + 20 min.
- Frontend nav is decided: Pools (read-only home) · Swap · Liquidity · Borrow · Portfolio · Liquidations (keeper) · Govern.
web3/FRONTEND.mdowns nav + reference,ui-flows.mdowns per-flow detail. - The Liquidations tab is the only surface needing off-chain infra: a small indexer over
Borrow/DepositCollateralevents +Lenshealth batches (polledeth_getLogsat launch, subgraph later). Rule everywhere else: events for history, Lens for live state.
04 The flows, drawn
Each maneuver is one transaction across four actors
The same four lanes appear in every figure — User, Router, Unimod (AMM), Lending. Reading left to right is reading the transaction. Hover any step or cell for detail.
What each entry point touches
Router entry points × protocol surfaces · ⟲ = fires a callback into the Router
The three flash composites are the only flows that cross the whole protocol — and they are exactly the ones bundled into a single click.
One-time approvals before first action
on-chain approvals per role (per token) · lending auth rides along as a signature, not a transaction
0 · 1 · 2 · 3 approvals →
A liquidator needs zero approvals and zero capital — the entire close is funded by the seized LP inside the transaction.
05 Protocol mechanics
The choices that make it oracle-free
One constraint drives most of the design: lending never reads a price. Health, liquidation, and collateral valuation all derive from pool balances the borrower can actually redeem — so manipulating the AMM's price cannot touch anyone's loan.
Per-asset, price-free health
For each borrowed asset i: borrowedᵢ ≤ ltv·s·bᵢ — the borrower's LP share s times the pool's balance bᵢ is their redeemable claim.
Why — no oracle to manipulate, no cross-asset pricing assumption. A swap can tilt balances, but the liquidity guard + buffer bound it, and any liquidation it forces is in-kind (locked in by TiltToLiquidate.t.sol).
Borrow buffer: 5%
Borrow and collateral-withdraw check against lltv − 0.05; only the liquidation trigger uses full lltv (default 0.80).
Why — a fresh max-borrower shouldn't sit on the liquidation edge; the gap absorbs normal balance drift.
Gentler liquidations than Morpho
Morpho's incentive formula, but cursor 0.1 (vs 0.3) and a 10% cap (vs 15%) → ~2% bonus at the default lltv.
Why — liquidation here needs zero capital and zero approvals, so keepers don't need to be paid as much; borrowers keep more.
In-kind liquidation returns
Seized LP is burned proportionally; the borrowed-asset slice repays the debt (+bonus), every other slice goes back to the borrower untouched.
Why — swapping the borrower's remainder would require pricing it. In-kind keeps the whole path price-free and caps what a manipulator can extract at the bonus.
Zaps: Star only
zapIn/zapOut exist on Star pools; Delta reverts. Single-asset Delta entry = swap → proportional add, composed by the caller.
Why — Delta's pricing is pairwise; a separate zap primitive would create a second price path that can diverge from swap pricing and open arb. One price path, reviewable math.
Concentration parameter
Replaced the dead "leverage" param. In bps (default 500 = 5%); price multiplier is exp(c·√(v/V)/3) — lower = tighter pricing. maxLiquidityRatio (default 4) bounds post-swap imbalance.
Why — one interpretable knob for liquidity concentration, and the imbalance bound doubles as the lending-side tilt guard.
Fees: LPs first
Swap fees accrue entirely to LPs inside reserves. The protocol's cut (protocolShare) applies to the mint fee only.
Why — keeps swap pricing clean and makes LP returns legible; protocol revenue rides growth, not volume.
SDK split, ABIs one-way
A separate unimod-sdk repo (seeded from web3/examples); ABI codegen and reference docs stay in this repo, pinned to the same SHA as the contracts.
Why — the SDK can iterate at frontend speed while the ABI source of truth stays next to the Solidity it describes. ABIs flow contracts → SDK, never backwards.
No profitable atomic manipulation — shown in silico, locked in on-chain
The oracle-free health rule has exactly one candidate atomic attack: tilt-to-liquidate — buy an asset out of the pool to push a borrower's per-asset claim below their debt, liquidate, pocket the bonus, all in one transaction. Two marimo notebooks (notebooks/tilt_to_liquidate.py, tilt_liquidate_concentration.py) work the economics end-to-end, and a Foundry suite (TiltLiquidateArb.t.sol / TiltToLiquidate.t.sol) runs the identical scenario against the real SPM + lending as ground truth.
The result: pool size cancels out of the profit equation entirely, and with the restore-to-health close factor clamping the repayable amount and in-kind returns capping extraction at the bonus, the attacker's net is ≈ bonus − (1 − attacker_LP_share)·tilt_slippage — negative at every concentration for any realistic LP holder. Without the close factor, a max-borrower could be liquidated in full off a near-free tilt; the notebooks are what surfaced that asymmetry and drove the RCF design.
Residual, stated honestly — an attacker owning nearly the entire pool's LP recovers their own tilt slippage and can profit on a very tight pool; that regime is visible in the notebook sweeps and is a market-curation concern (don't borrow against pools with a dominant LP), not a contract bug.
06 Markets, fees & governance
Two market segments, three thin fee taps
The protocol runs a permissionless segment and a sponsored segment side by side, on the same contracts. Governance surface is deliberately small: curation and fee knobs, never the risk math.
Anyone creates a pool over any registered assets — no approval step. Each pool's lending markets are per-pool, per-asset: risk is fully isolated, one bad pool can never contaminate another. LLTV, IRM and lending fee start at protocol defaults (LLTV 0.80, adaptive-curve IRM) and can be tuned per isolated market by the protocol owner.
One honest asterisk: the asset registry itself is protocol-owner gated (registerAsset) — permissionless means any pool over the registered universe, not arbitrary tokens. A deliberate rug/weird-token filter at the front door.
Anyone can propose a market (createMarket); it goes live once the protocol owner enables it. From there the sponsor (market owner) curates their venue: whitelist assets, allow Star and/or Delta pools, enable / disable / destroy pools, freeze the market, transfer ownership.
The structural difference from the isolated segment: lending markets are shared across all pools in the market — one supply-side per asset serving the sponsor's whole pool family. That's the venue for a curator who wants deeper, unified lending liquidity behind a vetted asset set.
Where the protocol takes fees — and where it deliberately doesn't
| Fee | Charged on | Goes to | Protocol's cut |
|---|---|---|---|
| Swap fee | Every swap, per pool | 100% to LPs — accrues inside pool reserves | Zero. Swaps are deliberately protocol-free to keep pricing competitive |
| Mint fee | LP minting (zaps pay swap/2 + mint) | LPs, minus the protocol slice | protocolShare of the mint fee, minted as LP shares to feeRecipient |
| Lending fee | Interest accrual, per market | Suppliers, minus the protocol slice | Per-market fee on accrued interest, minted as supply shares to the lending feeRecipient |
The pattern in both taps: the protocol is paid in the same instrument as the participants it sits behind — LP shares on the AMM side, supply shares on the lending side — so protocol revenue is staked to the same pools and markets it's extracted from, and rides growth rather than volume.
- Governance surface, in full — protocol owner: asset registry, market approval, fee levels + recipients, SPM/IRM assignment per market, and an SPM off-switch (
DisableSpm); sponsor: whitelist + pool lifecycle within their market. Everything covered byAccessControl.t.sol. - What no one governs — the health rule, liquidation formula, borrow buffer, and share accounting are constants in code. No parameter exists that could re-price collateral or move the liquidation trigger for existing positions beyond the capped
lltvsetters.
07 Interface & identity
Two live explorations
Brand and product are being explored in parallel, in separate deployments — the identity work feeds the product UI, not the other way round.
Generative identity built on Conway's Game of Life — gliders, oscillators and still-lifes as visual primitives under the thesis "conserved under transformation." Monochrome wordmark at the core, 15 procedural palettes, live parameter controls (font, palette, noise, intensity, speed). Works offline; deployed from unimod-design on Cloudflare Pages. The background of this page runs the same automaton.
First functional pass at the dapp (React/Vite SPA, wallet-connected): pools, swap, supply / borrow / collateral, and positions — tracking the decided tab structure and built against the web3/examples + Lens read patterns. Tentative by design: it exists to pressure-test the UX paths in section 03 before committing to the final frontend.
08 Next
The path to mainnet is hardening + surface
- Security — external audit, and a decision on how to prove (not just fuzz) the two properties everything rests on: the accounting invariants and no-arbitrage. The candidates, with their trade-offs:
- Certora Prover — CVL specs over the share-accounting and solvency invariants. The strongest fit for the lending half: Morpho Blue itself is Certora-verified, so specs can be adapted rather than written from scratch. Cost: commercial licence + spec-writing effort.
- Halmos (revisit) — free bounded symbolic execution; worked for V1 and was retired with it. Good for exhaustive small-depth checks (auth, casts, settlement netting); the CI-minutes problem is solvable by running it on-demand rather than per-push.
- Kontrol / SMTChecker — K-framework semantics for deeper proofs, or solc's built-in checker for cheap local assertions. Both worth a pilot, neither a full answer alone.
- The shared limitation — every SMT-based tool struggles with the SPM's transcendental math (
exp(c·√(v/V)/3)via Padé). The realistic plan is a hybrid: machine-check the accounting invariants (linear, shares) with Certora/Halmos, and handle no-arbitrage at the math level. - No-arbitrage at the math level — prove the round-trip-loses property of the pricing function itself (pen-and-paper first, optionally formalized in Lean4/mathlib), then bound the Solidity approximation error separately —
ExpSqrtComparison.t.solalready measures the Padé gap, and the tilt notebooks + mirrored Foundry suites stay as the economic regression harness for everything the proofs idealize away.
- Contract surface — native ETH support on the Router (WETH wrap),
removeLiquidityExactLp, optional Permit2SignatureTransferpull (gasless one-shot approvals; deliberately deferred —AllowanceTransferis the right default for active users). - Off-chain — the liquidation-candidate indexer (polled logs first, subgraph later); stand up
unimod-sdkfrom the examples. - Product — converge the design-system exploration and the tentative UI into one frontend; incentives/liquidity-mining design.