Sepolia

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:

  1. Approving the pool as a lot operator on the app once (app.setOperator(pool, true));
  2. Calling pool.borrow(vaultIds, bid, curatorSig, minPrincipal, receiver).

Inside pool.borrow, the contract verifies:

  • The caller is the origin of every lot in vaultIds;
  • bid.lender == address(pool) and bid.loanToken == address(LOAN_TOKEN);
  • curatorSig is a valid signature by CURATOR (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 (650 bps = 6.50% on Sepolia);
    • bid.debtPerBtc <= maxDebtPerBtc ($65,000 on 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 of JUNIOR_YIELD_PREMIUM_BPS (20% of Senior's interest goes to Junior as a risk premium for standing first-loss).
  • On default (claimDefaultedLoan(groupId) then buyDefaultedCollateral(groupId, maxPayment)) — once maturity passes unpaid, anyone triggers claimDefaultedLoan and a 24-hour (AUCTION_DURATION) Dutch auction that starts at debt and decays linearly toward 1. 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 raise seniorTarget (S0) and a fixed maturity face value seniorFace (F >= S0).
  • J_TOKEN (J) — Junior first-loss equity with a target raise juniorTarget (J0), entitled to 100% of the excess spread above F and 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:

  1. Cash first to Senior — seniorCash = min(totalCash, seniorFace), juniorCash = totalCash - seniorCash.
  2. Collateral shortfall cover at priceFloor — if seniorCash < seniorFace and unsoldCollateral > 0, the unpaid Senior shortfall seniorFace - seniorCash is converted into collateral at priceFloor (rounded up in Senior's favor via mulDivCeil, capped at unsoldCollateral). Junior receives any remaining unsoldCollateral.

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:

  1. 0. Zero deployment (all idle) — 0% lent; Senior receives min(S0 + J0, F) (the idle subsidy F - S0 is paid out of Junior's principal and capped at maxIdleSubsidy <= J0).
  2. 1. Disclosed deployment, repaid at min APR — disclosedDeployBps of the pool lent at minAprBps and repaid in full; Senior receives full F in cash and Junior receives J0 + net interest.
  3. 2. Disclosed deployment, 100% default at floor — disclosedDeployBps lent at maxDebtPerUnit and 100% defaulted at priceFloor.
  4. 3. Worst case (max deployable), 100% default at floor — maximum deployable capital (min(100%, maxLoans * maxLoanBps)) lent at maxDebtPerUnit and 100% defaulted at priceFloor.

Configuration floors

  • Every time window (fundingDeadline - now, investmentDeadline - fundingDeadline, gracePeriod, recoveryDeadline - maturity - gracePeriod, maxInvestmentWindow) must be >= MIN_WINDOW (1 hour).
  • Loan term maturity - investmentDeadline must be >= MIN_LOAN_TERM (7 days), ensuring minAprBps always 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.