Direct Earn
Two ways for passive capital to lend on a Bivium Direct app (Architecture), both in bivium-tbv-direct, and both completely oracle-free. Each splits lenders into a Senior tranche paid first and a Junior tranche that absorbs losses first. Instead of relying on external put hedges anchored to an off-chain index, Bivium Direct protects Senior capital through Junior's on-chain first-loss buffer and deterministic loan mandates.
| Feature | DirectEarnPool |
DirectSeries |
|---|---|---|
| Structure | Open-ended, continuous liquidity pool | Closed-end, fixed-term pool with a single maturity T |
| Senior holds | seniorShares in the pool |
S, a transferable ERC-20 zero-coupon bond with fixed face F |
| Junior holds | juniorShares in the pool (first loss on every loan) |
J, a transferable ERC-20 claim on all residual cash & collateral after F |
| Origination | Curator signs LendBids with lender = pool; borrowers take them via pool.borrow(...) |
Curator fills borrowers' signed BorrowIntents via series.fundLoan(...) |
| Exit before maturity | Withdraw from currently idle tranche cash (withdrawSenior / withdrawJunior) |
Transfer or trade ERC-20 S or J; pool redemption opens at Finalized |
| Default recovery | Per-loan 24-hour Dutch auction from debt down to 1 |
Single aggregate Dutch auction across [decayStart, recoveryDeadline], then physical distribution |
DirectEarnPool
One continuous pool per GroupEngine app and loan token (tbvEarnPool on TbvDirectApp,
erc20EarnPool on Erc20DirectApp). Lenders call depositSenior(assets) or depositJunior(assets)
to mint tranche shares and withdrawSenior(shares) / withdrawJunior(shares) to redeem against that
tranche's idle cash (seniorIdleCash / juniorIdleCash).
Curator quotes, contract enforces the mandate
The pool's CURATOR signs standard EIP-712 LendBids with lender = address(pool) and posts them to
/api/direct-orders. A borrower takes a pool bid by:
- Approving the pool as a lot operator on the app once (
app.setOperator(pool, true)); - Calling
pool.borrow(vaultIds, bid, curatorSig, minPrincipal, receiver).
Inside pool.borrow, the contract verifies:
- The caller is the
originof every lot invaultIds; bid.lender == address(pool)andbid.loanToken == address(LOAN_TOKEN);curatorSigis a valid signature byCURATOR(ECDSA or EIP-1271);- The loan satisfies the pool's immutable and governance-bounded mandate:
maturity - block.timestamp <= MAX_LOAN_DURATION;- annualized rate meets
MIN_APR_BPS(650bps =6.50%on Sepolia); bid.debtPerBtc <= maxDebtPerBtc($65,000on Sepolia);- both Senior and Junior tranches have sufficient idle cash (
deployablePrincipal) to fund their mandated split (JUNIOR_TRANCHE_BPS = 2000=20%Junior,80%Senior).
The pool then sets a transient one-transaction authorization flag, approves principal to the app,
and calls app.borrowWithBid(...). Outside of pool.borrow, the pool's EIP-1271 isValidSignature
returns 0xffffffff, guaranteeing that a curator-signed LendBid can never bypass the pool's
mandate checks by being submitted directly to app.borrowWithBid.
Waterfall and default auction
Every loan funded by DirectEarnPool is backed 80% by Senior principal and 20% by Junior
principal (JUNIOR_TRANCHE_BPS):
- On repayment (
settleRepaidLoan(groupId)) — Senior receives its principal plus its share of interest net ofJUNIOR_YIELD_PREMIUM_BPS(20%of Senior's interest goes to Junior as a risk premium for standing first-loss). - On default (
claimDefaultedLoan(groupId)thenbuyDefaultedCollateral(groupId, maxPayment)) — oncematuritypasses unpaid, anyone triggersclaimDefaultedLoanand a 24-hour (AUCTION_DURATION) Dutch auction that starts atdebtand decays linearly toward1. Auction proceeds pay Senior's principal + interest first, and any remainder goes to Junior.
DirectSeries: a fixed-term ZCB
A DirectSeries is a self-contained, closed-end credit series with fixed dates that sits on a
GroupEngine app as an ordinary lender. It issues two ERC-20 tokens (SeriesToken):
S_TOKEN(S) — Senior zero-coupon bond with a fixed target raiseseniorTarget(S0) and a fixed maturity face valueseniorFace(F >= S0).J_TOKEN(J) — Junior first-loss equity with a target raisejuniorTarget(J0), entitled to 100% of the excess spread aboveFand absorbing defaults first.
Before activate(), SeriesToken transfers are locked (SeriesNotActive); once activate()
succeeds, S and J are freely transferable ERC-20 tokens.
Lifecycle phases
| Phase | Enum | What happens |
|---|---|---|
| Funding | 0 |
Users call subscribe(isJunior, amount) up to seniorTarget and juniorTarget. Senior subscriptions must be a multiple of seniorLot (sLot) so every subscription maps to an exact integer number of S units (seniorUnits). Before fundingDeadline, the curator must subscribe at least curatorMinimum(juniorTarget, curatorJuniorBps) into Junior, which stays locked until finalization |
| Investing | 1 |
Entered via activate() once both tranches are 100% subscribed. Subscribers call claim(isJunior) to mint their ERC-20 S or J tokens. The curator calls fundLoan(intent, sig, principal) on borrowers' signed BorrowIntents. Every funded loan must mature at <= maturity, stay within maxDebtPerUnit and maxLoanBps, and earn at least minAprBps |
| Holding | 2 |
Entered at investmentDeadline (advancePhase()). No new loans can be originated; repayments accumulate in confirmedCash via processLoans |
| Recovering | 3 |
Entered at maturity (advancePhase()). processLoans(maxCount) settles repaid loans and claims defaulted collateral from the app. Once recoveryComplete is true and gracePeriod (G) has elapsed (auctionReadyAt), a single Dutch auction (buyCollateral(collateralAmount, maxCost, to)) sells defaulted collateral from priceStart (debtDefaulted / collateralDefaulted) down to priceFloor (priceStart * floorBps / 10000) until recoveryDeadline (R) |
| Finalized | 4 |
Entered via finalize() once all collateral is sold or recoveryDeadline is reached. Freezes finalAllocation(false) (Senior) and finalAllocation(true) (Junior). Token holders call redeem(isJunior, amount, to) to burn S or J for their pro-rata cash + collateral |
| Cancelled | 5 |
Entered if fundingDeadline passes before activate(), or if maxInvestmentWindow expires with zero loans deployed. Subscribers call refund(isJunior) to recover 100% of their deposited USDC |
The SeriesMath waterfall and four disclosure scenarios
At finalize(), SeriesMath.splitWaterfall(totalCash, unsoldCollateral, seniorFace, priceFloor, unit, hasDefault)
computes the exact allocation between Senior S and Junior J:
- Cash first to Senior —
seniorCash = min(totalCash, seniorFace),juniorCash = totalCash - seniorCash. - Collateral shortfall cover at
priceFloor— ifseniorCash < seniorFaceandunsoldCollateral > 0, the unpaid Senior shortfallseniorFace - seniorCashis converted into collateral atpriceFloor(rounded up in Senior's favor viamulDivCeil, capped atunsoldCollateral). Junior receives any remainingunsoldCollateral.
Because no oracle is used, DeployDirectSeries.s.sol and the v3.cai.finance Earn Pro view compute
and display four deterministic disclosure scenarios directly from the 24 immutable Terms:
- 0. Zero deployment (all idle) —
0%lent; Senior receivesmin(S0 + J0, F)(the idle subsidyF - S0is paid out of Junior's principal and capped atmaxIdleSubsidy <= J0). - 1. Disclosed deployment, repaid at min APR —
disclosedDeployBpsof the pool lent atminAprBpsand repaid in full; Senior receives fullFin cash and Junior receivesJ0+ net interest. - 2. Disclosed deployment, 100% default at floor —
disclosedDeployBpslent atmaxDebtPerUnitand 100% defaulted atpriceFloor. - 3. Worst case (max deployable), 100% default at floor — maximum deployable capital
(
min(100%, maxLoans * maxLoanBps)) lent atmaxDebtPerUnitand 100% defaulted atpriceFloor.
Configuration floors
- Every time window (
fundingDeadline - now,investmentDeadline - fundingDeadline,gracePeriod,recoveryDeadline - maturity - gracePeriod,maxInvestmentWindow) must be>= MIN_WINDOW(1 hour). - Loan term
maturity - investmentDeadlinemust be>= MIN_LOAN_TERM(7 days), ensuringminAprBpsalways accrues over at least one week. seniorFace - seniorTarget <= maxIdleSubsidy <= juniorTarget.
On Sepolia, the canonical DirectSeries (0x9a3762FfcDA752EabA3d2b1BbFefBD2e8751f772, with S_TOKEN
0xc786346B585186B36763C49C20c2b6CB66cb0474 and J_TOKEN 0xC3827f27a4ee42d2e749D02678c98524ED7B3cba)
was deployed from bivium-tbv-direct@bd7e8a8 with a 27-day loan window ($100,000 Senior target,
$105,000 Senior face, $25,000 Junior target, 8.00% min APR, $60,000 max debt/BTC).
Vendored from bivium-protocol-doc 23b9b35958be, synced 2026-10-27. Where this and a repository disagree about that repository, the repository wins.