C0LLBACK
c0llback/strata

A simplified, modular lending pool. Supply to earn, post collateral to borrow — and swap the interest, liquidation and pricing policies on a live market without migrating a balance.

soliditydefilendingmoney-marketliquidationinterest-rate-modelethereum
Solidity 0.6.12License MITNetwork Ethereum16 files
Solidity 80.2%Markdown 17.5%Text 2.3%
c0llback7d1b4f0pool: pluggable interest model — fixed + kinked utilization curveFeb 22, 2021
README.md

STRATA

Solidity License Build Coverage

A simplified, modular lending pool. Supply an asset to earn yield, post collateral to borrow it — and swap the interest, liquidation and pricing policies on a live market without migrating a single balance.

Compound and Aave proved the money-market shape by 2020. The part worth re-studying isn't the accounting — it's that the policy (how rates are set, when a position is unsafe, how collateral is priced) lives in swappable strategy contracts, not welded into the pool. STRATA keeps the core tiny and immutable and pushes all three policies behind interfaces:

PolicyInterfaceSwappable?Ships with
Interest rateIInterestRateModelFixedRateModel, UtilizationRateModel
Risk / liquidationILiquidationModuleLiquidationModule (LTV, threshold, bonus…)
PricingIPriceOracleSimplePriceOracle (swap in Chainlink later)

Governance can move a market from a fixed rate to a utilization-driven curve, tighten the LTV before a volatile event, or upgrade the oracle — each is a one-transaction module swap on the running pool.


How it works

SUPPLIERS POOL CORE BORROWERS
───────── ┌──────────────────────┐ ─────────
supply(debt) ───────────→ │ cash + borrows │ ←─ borrow(debt)
redeem(shares)←─────────── │ - reserves │ ── repay(debt) ─→
│ │
│ borrowIndex (compounds)
│ supply shares (exchange rate floats up)
└──────────┬────────────┘
│ reads policy from…
┌──────────────────────┼───────────────────────┐
↓ ↓ ↓
IInterestRateModel ILiquidationModule IPriceOracle
rate = f(utilization) LTV / threshold / price(collateral)
closeFactor / bonus price(debt)

Interest accrues Compound-style. A global borrowIndex compounds the debt side at every interaction:

interestFactor = ratePerSecond × secondsElapsed
totalBorrows += interestFactor × totalBorrows
borrowIndex += interestFactor × borrowIndex // borrowers' debt grows with this
totalReserves += interestFactor × totalBorrows × reserveFactor

A borrower's live debt is principal × borrowIndex / indexAtLastTouch. Suppliers hold shares; the exchange rate (cash + borrows − reserves) / shares drifts upward as interest lands, so redeeming returns more than was supplied.

Health. A position is solvent while

healthFactor = collateralValue × liquidationThreshold / debt ≥ 1.0

Borrowing is gated by the (lower) ltv; liquidation triggers at the (higher) liquidationThreshold. The band between them is the borrower's margin of safety.

Liquidation. Once healthFactor < 1, anyone may repay up to closeFactor × debt and seize collateral worth the repayment plus the liquidationBonus:

collateralSeized = (repayAmount / collateralPriceInDebt) × (1 + liquidationBonus)

The two interest models

UtilizationRateModel is the kinked "JumpRate" curve — cheap while the pool has liquidity, punitive once it's nearly drained, so the market self-corrects toward a healthy utilization:

rate
│ ╱ slope2 (scarcity premium)
│ ╱
│ slope1 ____╱ ← kink (e.g. 80%)
│ ____╱
│ base ____╱
└──────────────────┴──────── utilization → 100%

FixedRateModel ignores utilization entirely — one flat APR. Useful to bootstrap a market, then setInterestModel(variable) once there's real supply and demand.


Layout

contracts/
├─ LendingPool.sol core market — accrual, supply/borrow/liquidate
├─ access/
│ ├─ Ownable.sol governance owner
│ └─ ReentrancyGuard.sol single-slot lock on every entrypoint
├─ interfaces/
│ ├─ IERC20.sol
│ ├─ IInterestRateModel.sol
│ ├─ ILiquidationModule.sol
│ └─ IPriceOracle.sol
├─ lib/
│ ├─ SafeMath.sol overflow-checked maths (pre-0.8.0)
│ ├─ WadMath.sol 1e18 fixed-point (wmul / wdiv)
│ └─ SafeERC20.sol tolerates non-standard tokens (USDT, …)
├─ models/
│ ├─ FixedRateModel.sol flat APR
│ └─ UtilizationRateModel.sol kinked two-slope curve
└─ modules/
├─ LiquidationModule.sol LTV / threshold / closeFactor / bonus + seize math
└─ SimplePriceOracle.sol owner-posted prices

Usage

// 1. Policy modules (per-second rates derived from APR wads inside the models)
UtilizationRateModel rates = new UtilizationRateModel(
0.02e18, // 2% base APR
0.08e18, // +8% APR by the kink
1.00e18, // +100% APR from kink to 100% utilization
0.80e18 // kink at 80%
);
LiquidationModule risk = new LiquidationModule(
0.75e18, // LTV 75%
0.80e18, // liquidation threshold 80%
0.50e18, // close factor 50%
0.08e18 // 8% liquidation bonus
);
SimplePriceOracle oracle = new SimplePriceOracle();
oracle.setPrice(address(WETH), 1800e18); // unit of account is arbitrary…
oracle.setPrice(address(USDC), 1e18); // …only the ratio matters
 
// 2. The market: collateral = WETH, debt = USDC, 10% reserve factor
LendingPool pool = new LendingPool(WETH, USDC, rates, risk, oracle, 0.10e18);
 
// 3. Flow
pool.supply(50_000e18); // a lender funds the USDC side
pool.depositCollateral(10e18); // a borrower posts 10 WETH
pool.borrow(12_000e18); // draws USDC, must stay under 75% LTV
pool.repay(borrower, 12_000e18); // ...later, with interest
 
// 4. Governance swaps policy on the live pool — no migration
pool.setInterestModel(rates); // fixed -> variable
risk.setParameters(0.70e18, 0.78e18, 0.5e18, 0.10e18); // tighten before volatility

Build & test

npm install
npx hardhat compile
npx hardhat test

Security

  • Not audited. Study build, published for review — no real value on mainnet without an audit.
  • Simplifications, stated plainly: single market; both assets assumed 18-decimal; no bad-debt socialisation (a liquidation reverts if it would seize more than the borrower's collateral); rates are linear-per-second, compounded only at accrual points.
  • Every state-changing entrypoint is nonReentrant and follows checks-effects-interactions; collateral is moved only after debt is written down.
  • The oracle fails closed — an unset/zero price reverts rather than valuing collateral at zero.
  • SafeERC20 tolerates tokens that don't return a bool. Fee-on-transfer / rebasing tokens are not supported as the debt asset (the accounting assumes received amount == stated amount).
  • Trust assumptions: the owner (governance) can swap modules and the oracle — i.e. it can reprice risk for everyone. Hand it to a timelock/multisig.

License

MIT © 2021 c0llback

← Back to index© 2020 c0llbackMIT