Posts

You Could Have Invented Morpho Optimizer

A layer-by-layer derivation of Morpho-Compound and Morpho-AaveV2.

A constructive tutorial

Morpho Optimizer was the original Morpho product, deployed in June 2022 as a peer-to-peer matching layer on top of Aave V2 and Compound V2. It pulled in over $2B in deposits in just over a year, but the design had a structural ceiling: it could only ever capture a fraction of the pool it sat on, and it inherited every limitation of the underlying protocol. That recognition is what drove the team to build Morpho Blue in early 2024 — a standalone lending primitive with no underlying pool to optimize against. The Optimizer itself was wound down through 2025: front-end retired in September, on-chain deprecation finalized in December.

Chapter 1

What lending pools actually do

Suppose you want to build an on-chain lending market for USDC. The simplest possible design is a shared pot:

  • Anyone can deposit USDC into the pot. They become a supplier.
  • Anyone can lock collateral (say, ETH) and pull USDC out of the pot. They become a borrower.
  • Suppliers earn interest from borrowers. Borrowers pay interest into the pot.

This is, mechanically, what Aave V2 and Compound are. The “pot” is a single contract per asset. Every supplier’s USDC is fungibly mixed with every other supplier’s USDC. Every borrower draws from the same pile.

THE POOL MODEL — ONE CONTRACT, TWO ROLESSuppliersUSDC POOL$100 insidesingle contract$80 lent · $20 idleBorrowersdeposit USDCborrow USDC
The pool model — everyone deposits into and borrows from one contract.

Now we hit the first practical question. How does a supplier know how much interest they’ve earned? Updating every supplier’s balance every block is gas-prohibitive — there could be tens of thousands of them.

The trick that Compound (and Aave) invented is the index (Compound calls it exchangeRate; Aave calls it liquidityIndex). It’s one number per market that grows monotonically over time at the supply APY:

poolSupplyIndex over time:

t=0     1.000e18
t=1y    1.030e18   (3% APY)
t=2y    1.061e18
t=3y    1.092e18

When you supply 100 USDC at index = 1.000e18, the contract records your scaled balance as 100 / 1.000 = 100. That number never changes until you supply or withdraw again.

A year later, the index has grown to 1.030. Your underlying claim is now:

underlying = scaledBalance × currentIndex
           = 100 × 1.030
           = 103 USDC

You earned 3 USDC and nobody touched your balance. The pool just ticked one number. Borrowers work the same way in reverse: a global borrowIndex grows at the borrow APY, and each borrower’s debt is a scaled “principal” that’s multiplied by the current index to get what they owe.

Mental model to lock in. Everywhere in DeFi lending, you’ll see:

// scale ⇄ underlying conversion
underlying = scaledBalance × index;
scaledBalance = underlying / index;

This is how a single global counter does interest accrual for everyone.

Chapter 2

Where the spread comes from

Here’s the part that motivates everything else. Open any Aave or Compound dashboard and you’ll see something like:

USDC market:
  Supply APY: 3.0%   ← what suppliers earn
  Borrow APY: 5.0%   ← what borrowers pay

Why the 2 percentage point gap? Where does the money go?

The arithmetic

Imagine the USDC pool has $100 of supply and $80 borrowed. Utilization = 80%. Suppose the borrow APY is 5%. Borrowers pay 80 × 5% = $4 in interest per year.

That $4 has to be divided across all $100 of supply, not just the borrowed portion. So:

supplyAPY ≈ borrowAPY × utilization × (1 − reserveFactor)
          = 5% × 80% × (1 − 10%)
          = 3.6%

The 10% reserveFactor is a tax kept by the protocol treasury. The rest of the gap is structural: the 20% of supply that’s idle earns nothing, but its yield gets averaged in with the 80% that’s productive.

SUPPLY SIDE — $100 USDC$80 lent outborrowers pay 5% APY$20 idleearns 0%$100 OF SUPPLY$4.00/yr interest ÷ $100 = 3.6% supply APYAlice and Bob could split the 1.4% spread if they met directly
The pool spread is not greed — it pays for the buffer that keeps withdrawals working.

This spread is structural. Even if the protocol set reserveFactor = 0, suppliers would still earn less than borrowers pay — because some supply must stay idle to honor withdrawals, and you can’t run a pool at 100% utilization without locking everyone out.

The thing to notice

If you’re Alice (supplying) and Bob (borrowing), and you could find each other directly, you could split the spread. Pick any rate between 3.6% and 5.0%, and:

  • Alice earns more than 3.6% (her pool APY).
  • Bob pays less than 5.0% (his pool APY).

Both win. The “buffer for idle liquidity” disappears when there’s a real counterparty for each dollar.

Chapter 3

The matchmaker idea

Here’s the clever bit. We don’t want to replace Aave or Compound. They already solved hard problems (oracles, collateral parameters, liquidations, an audited risk framework). We just want to sit on top of the pool and pair people up opportunistically.

The simplest possible matchmaker:

function supply(amount):
    if there's an existing pool borrower for this asset:
        match them with us
        repay their pool debt on their behalf
        record: we are matched to them, both at the P2P rate
    else:
        just deposit to the pool like normal

function borrow(amount):
    if there's an existing pool supplier:
        match us with them
        withdraw their pool supply on their behalf, give it to us
        record: we are matched to them
    else:
        just borrow from the pool

That’s the kernel. The matchmaker promises:

  1. If matched: both sides get a better rate than the pool.
  2. If not matched: use the pool, same outcome as going directly.

Now let’s start adding the layers it actually needs.

Layer 1

Two buckets per user

A user might be partially matched and partially on the pool. If Alice supplies $100 and only one $60 borrower exists, $60 of Alice is matched (earning the P2P rate) and $40 is parked on the pool (earning the pool rate). We need to track both.

So we split each user’s position into two scaled balances per market:

struct SupplyBalance {
    uint256 onPool;   // scaled against poolSupplyIndex
    uint256 inP2P;    // scaled against p2pSupplyIndex
}

struct BorrowBalance {
    uint256 onPool;   // scaled against poolBorrowIndex
    uint256 inP2P;    // scaled against p2pBorrowIndex
}

Alice’s underlying claim is now:

underlying = onPool × poolSupplyIndex   +   inP2P × p2pSupplyIndex
              ╰────────────────────╯       ╰─────────────────────╯
              parked-on-Compound part       matched-to-Bob part
              grows at the pool rate        grows at the P2P rate
ALICE’S $100 SUPPLY — TWO BUCKETSMATCHEDinP2P · $60matched to Bobearns the P2P rate (≈ 4.2%)FALLBACKonPool · $40sitting in Compoundearns the pool rate (3.6%)Two buckets, each growing at its own rate, each scaled by its own index.
Every user is conceptually two positions stacked.

Total supply across the market is the sum of onPool balances (which represent cTokens Morpho actually holds in Compound) plus the sum of inP2P balances (which represent claims on matched borrowers).

Layer 2

Indexes, not balances

We can’t update every matched user’s balance every block when interest accrues. Same problem Compound solved with exchangeRate — we need our own version.

We already have two indexes from the pool (poolSupplyIndex, poolBorrowIndex) that handle interest for the onPool portions. For the inP2P portions, we introduce two more:

IndexOwned byGrows at
poolSupplyIndexCompound/Aavepool supply APY (e.g. 3.6%)
poolBorrowIndexCompound/Aavepool borrow APY (e.g. 5.0%)
p2pSupplyIndexMorphoP2P supply APY (e.g. 4.2%)
p2pBorrowIndexMorphoP2P borrow APY (e.g. 4.4%)

Morpho stores them per market. Every time anyone interacts with a market, Morpho reads the current pool indexes from Compound, computes the growth since the last update, and advances its own P2P indexes by the matching amount:

function updateP2PIndexes(market):
    poolSupplyGrowth = ICToken(market).exchangeRateCurrent() / lastPoolSupplyIndex
    poolBorrowGrowth = ICToken(market).borrowIndex()         / lastPoolBorrowIndex

    p2pGrowth = weightedAverage(poolSupplyGrowth, poolBorrowGrowth, cursor)
                  // the cursor is a per-market knob — Chapter 9

    p2pSupplyIndex *= p2pGrowth   // (minus reserve factor tweaks)
    p2pBorrowIndex *= p2pGrowth

    snapshotPoolIndexes()

Two consequences worth chewing on:

  1. Morpho has no rate model of its own. It reads the pool’s rates and computes a number in between. If Compound’s borrow rate spikes, Morpho’s P2P rate follows automatically.
  2. Indexes only tick on touch. Between actions, the indexes are stale — but so are lastPoolIndexes, so the growth ratio next time covers the entire missing interval. Conservation works out.
Layer 3

A sorted linked list for matching

“Find an existing pool borrower to match against” sounds simple. In Solidity, you can’t iterate a mapping. We need a data structure that lets us iterate users in a market and mutate cheaply.

Maintain four lists per market:

mapping(market => DoubleLinkedList) suppliersInP2P;
mapping(market => DoubleLinkedList) suppliersOnPool;
mapping(market => DoubleLinkedList) borrowersInP2P;
mapping(market => DoubleLinkedList) borrowersOnPool;

Why a doubly-linked list? Because when a user gets matched or unmatched, they need to be removed from one list and inserted into another. A doubly-linked list does removal in O(1) given a node pointer.

Why sorted by amount (largest first)? Because when a new borrower wants $1000 and the pool has many small suppliers, you want to start with the biggest one. Matching against one $800 supplier and one $200 supplier = 2 iterations. Matching against a thousand $1 suppliers = 1000 iterations (or worse, OOG revert).

SUPPLIERSONPOOL[USDC] — SORTED, LARGEST FIRSTAlice$800Carol$300Dan$150Eve$40Bob wants to borrow $1000 → walk from the head, slice Alice ($800) + $200 of Carol’s $300. Done in 2 steps.
The linked list lets matching cover large amounts in few iterations.

Insertion into a sorted list is O(n) in general, so Morpho caps it: there’s a constant maxSortedUsers, and once a user’s position passes that threshold of being-larger-than-others, they get inserted at the head region; smaller positions accumulate near the tail. It’s a pragmatic compromise — perfect sorting would cost too much gas at insert time.

Layer 4

What happens when someone leaves

Supplying and borrowing are easy. The hard part is exit. Suppose:

  • Alice supplied $100. No borrowers existed, so her $100 is all onPool.
  • Bob deposits collateral, borrows $50. Morpho matches him with Alice: Alice now has onPool=$50, inP2P=$50, Bob has inP2P=$50.
  • Alice calls withdraw(100).

Alice’s first $50 is easy — just redeem her cTokens from Compound. But her second $50 is in P2P, matched to Bob. What do we do with Bob?

Option A: Find a replacement supplier (promote)

Search suppliersOnPool for someone who can take Alice’s slot. If Carol has $200 sitting on the pool, we can:

  1. Move $50 of Carol’s position from onPool to inP2P. (Pure accounting flip.)
  2. Redeem $50 of Carol’s cTokens from Compound. Hand the cash to Alice.

Bob is still in P2P, but now matched against Carol instead of Alice. Bob doesn’t even know.

REPLACEMENT MATCH — PROMOTE CAROLBEFORE — ALICE STILL HEREAliceinP2PmatchedBobinP2PCarolonPoolAFTER — ALICE EXITS, CAROL PROMOTEDAliceexitsBobinP2PmatchedCarolinP2P
Replacement supplier — Bob keeps his P2P match, just with a different counterparty.

Option B: Demote Bob to the pool

If no replacement supplier is available, we have to push Bob back to the pool. “Demoting” Bob means:

  1. Bob’s inP2P decreases by $50 (accounting).
  2. Bob’s onPool increases by $50 (accounting).
  3. Morpho calls borrow() on Compound, pulling $50 of USDC out. (Bob’s collateral is already on Compound, so Morpho’s contract-level account is collateralized.)
  4. That $50 goes to Alice.

Bob’s underlying debt is unchanged — he still owes $50. What changed is which bucket holds it, and therefore which rate it accrues at. Bob’s effective borrow rate just rose from the P2P rate to the (worse) pool rate. From his perspective, his “discount evaporated.” Tough.

“Demoting” and “promoting” are pure accounting moves — they only flip onPool and inP2P on a single user. The actual borrow / repay / supply / withdraw calls to Compound happen separately, in bulk, after the accounting is settled.

Layer 5

Gas budgets and the delta

What if Alice was matched against 1000 small Bobs (each owing $1)? Demoting all of them in one transaction would cost more gas than the block limit allows. Without a fallback, the entire withdraw reverts and Alice is stuck.

The fix has two parts:

  1. Cap the work. Every matching loop carries a maxGasForMatching budget. When the budget is exhausted, the loop stops cleanly, returning how much it accomplished.
  2. Track the unfinished business. The residual — Bobs we wanted to demote but couldn’t — is accounted for in a special variable called the delta.

The delta state

struct Delta {
    uint256 p2pSupplyDelta;   // P2P-tagged supply that's parked on the pool
    uint256 p2pBorrowDelta;   // P2P-tagged borrow that's actually borrowed from the pool
    uint256 p2pSupplyAmount;  // total P2P supply, scaled
    uint256 p2pBorrowAmount;  // total P2P borrow, scaled
}

A p2pBorrowDelta = $700 means: “We claim there’s $700 of P2P borrow in our accounting, but it’s really backed by a Morpho-owned pool borrow, not by a matched supplier.”

Worked example

Indexes all equal 1.0 for simplicity. Suppose the gas budget allows exactly 3 demotion iterations.

Setup. Alice supplied $1000, got matched piece-by-piece against ten borrowers Bob1…Bob10, each borrowing $100.

Alice:   onPool=0,   inP2P=1000        (fully matched)
Bob1:    onPool=0,   inP2P=100
Bob2:    onPool=0,   inP2P=100
...
Bob10:   onPool=0,   inP2P=100

p2pSupplyAmount = 1000
p2pBorrowAmount = 1000
p2pSupplyDelta  = 0
p2pBorrowDelta  = 0

Morpho's Compound account:
  Supplied:   5 ETH (sum of Bobs' collateral)
  Borrowed:   $0
  cUSDC held: $0   ← Alice's USDC was all redeemed when Bobs borrowed

Alice calls withdraw(1000). The contract works through its cascade:

  1. Withdraw from Alice’s own onPool. She has 0. Skip.
  2. Eat p2pSupplyDelta. Is 0. Skip.
  3. Promote a pool supplier into Alice’s slot. suppliersOnPool is empty. Skip.
  4. Demote P2P borrowers. Loop starts. Demote Bob1 ($100), Bob2 ($100), Bob3 ($100). Gas budget runs out. Loop exits with unmatched = $300.

Residual = $1000 − $300 = $700. Code path:

if (unmatched < remainingToWithdraw) {
    delta.p2pBorrowDelta += (1000 - 300) / poolBorrowIndex;  // += 700
}
delta.p2pSupplyAmount -= 1000 / p2pSupplyIndex;  // → 0
delta.p2pBorrowAmount -= 300  / p2pBorrowIndex;  // → 700

_borrowFromPool(market, 1000);  // Morpho borrows the full $1000 from Compound
transfer(alice, 1000);

End state:

Alice:   gone
Bob1:    onPool=100, inP2P=0     (cleanly demoted)
Bob2:    onPool=100, inP2P=0
Bob3:    onPool=100, inP2P=0
Bob4:    onPool=0,   inP2P=100   ← STILL TAGGED P2P (they don't know yet)
Bob5:    onPool=0,   inP2P=100
...
Bob10:   onPool=0,   inP2P=100

p2pSupplyAmount = 0
p2pBorrowAmount = 700    ← Bob4..Bob10 still here
p2pSupplyDelta  = 0
p2pBorrowDelta  = 700    ← THE DELTA: $700 of phantom P2P backed by pool borrow

Morpho's Compound account:
  Supplied:   5 ETH
  Borrowed:   $1000 USDC   (= $300 backing Bob1..Bob3 + $700 backing the delta)

What the delta does to interest rates

Bob4..Bob10 think they’re in P2P. Their stored inP2P is unchanged. But economically, their debt is backed by a pool borrow. So when the indexes next update, Morpho blends:

shareOfDelta = (p2pBorrowDelta × poolBorrowIndex) / (p2pBorrowAmount × p2pBorrowIndex)
             = (700 × 1.0) / (700 × 1.0)
             = 1.0   ← 100% delta-covered right now

p2pBorrowIndex grows at:
   (1 − shareOfDelta) × p2pGrowthFactor  +  shareOfDelta × poolGrowthFactor
   = 0 × p2pGrowthFactor + 1.0 × poolGrowthFactor
   = pool borrow growth

So Bob4..Bob10 effectively pay the pool borrow rate while the delta is uncleared. No one had to touch their individual positions — the index does the work.

How the delta heals

The next time a supplier deposits, the supply flow has a step that says “first, use this new supply to pay down the pool borrow that backs the delta”:

if (delta.p2pBorrowDelta > 0) {
    // pay down the delta with the new supply
    repay_to_pool(min(newSupply, deltaInUnderlying));
    delta.p2pBorrowDelta -= ...;
}

If Eve supplies $500, the delta shrinks from $700 to $200. Bob4..Bob10’s effective rate shifts from “100% pool rate” toward “71% P2P, 29% pool”. The mismatch resolves itself proportionally as new traffic arrives.

The delta is not a loss. Morpho is never insolvent. The delta is a bookkeeping marker for “we couldn’t finish the matching in one tx — the index will price it as a pool position until cleaned up.”

The symmetric case: repay creates a supply delta

If a P2P borrower repays a huge amount and Morpho can’t find enough P2P suppliers to demote (gas-bounded), the freed supply gets parked on the pool, and a p2pSupplyDelta is recorded. Same mechanics, opposite direction. P2P suppliers’ rates drift toward the pool supply rate until the next borrower absorbs the delta.

Why deltas only appear on exit, not on entry

OperationFailure modeDelta?
SupplyNo borrowers to match → falls through to pool depositNo
BorrowNo suppliers to match → falls through to pool borrowNo
WithdrawCan’t fully demote matched borrowersp2pBorrowDelta
RepayCan’t fully demote matched suppliersp2pSupplyDelta

Entry operations always have a clean fallback (the pool itself). Exit operations must somehow disconnect existing matches, and disconnecting can be capped by gas — that’s the unique problem the delta solves.

Layer 6

Choosing the P2P rate

We’ve been waving at “the P2P rate” as something between pool supply and pool borrow. Where exactly? And how is the small remaining gap (if any) divided?

The cursor

Each market has a parameter p2pIndexCursor in basis points (0 to 10000). It picks the point between the pool supply rate and the pool borrow rate where the “fair” P2P rate sits:

THE SPREAD, RECARVEDmidpoint · 5000pool supply3.6%pool borrow5.0%p2pIndexCursor = 3333P2P rate · 4.07%cursor=0 → favors borrowers; cursor=10000 → favors suppliers; 5000 → midpoint
The cursor knob shifts the P2P rate along the spread.

Production values are usually around 3333 (one-third), slightly favoring borrowers, on the theory that borrowers initiate matches more often and incentivizing them grows the pie.

The reserve factor

The protocol’s DAO needs revenue to fund ops and pay for incidents. So there’s a tiny extra spread, controlled by reserveFactor:

p2pSupplyRate = p2pRate − (p2pRate − poolSupplyRate) × reserveFactor
p2pBorrowRate = p2pRate + (poolBorrowRate − p2pRate) × reserveFactor

If reserveFactor = 1000 (10%), suppliers earn slightly less than borrowers pay; the 10% sliver of the surplus accrues to Morpho. Visualized:

pool borrow ─●─                              5.00%


         ─ ─ ─ ─ ─ p2pBorrowRate            4.16%   ← Bobs pay this

         ─ ─ ─ ─ ─ midpoint (cursor)        4.07%

         ─ ─ ─ ─ ─ p2pSupplyRate            3.99%   ← Alices earn this


pool supply ─●─                              3.60%

reserve factor tax = p2pBorrowRate − p2pSupplyRate ≈ 0.17%

Still way better than the 1.4% pool spread.

Layer 7

Risk and liquidation, inherited

We’ve built a yield-optimization layer. We have NOT built collateral parameters, oracles, liquidation thresholds, or close factors. Reinventing those is a year of careful work.

Morpho’s solution: don’t reinvent them. Inherit them.

  • Collateral factor (LTV) per asset: read from Compound’s Comptroller / Aave’s risk params.
  • Oracle for collateral prices: Compound’s oracle / Aave’s oracle.
  • Liquidation threshold and incentive: the pool’s values.
  • Close factor (max % of debt repayable per liquidation): from the pool.

Two benefits:

  1. No new risk surface. A Morpho user’s risk profile is identical to using the pool directly. Same liquidation triggers, same protections.
  2. Composability with the pool. Morpho is itself one big user of the pool. The collateral it deposits there (on its users’ behalf) must satisfy the pool’s risk model, so it would have to use these parameters anyway.

Liquidation logic just wraps repay + withdraw internally:

function liquidate(borrower, collateralMarket, debtMarket, amount):
    require(_isLiquidatable(borrower));  // using pool's oracle + LTVs
    require(amount <= debt × pool.closeFactor());

    _repayLogic(debtMarket, liquidator, borrower, amount);
    amountToSeize = amount × pool.liquidationIncentive() × debtPrice / collateralPrice;
    _withdrawLogic(collateralMarket, amountToSeize, borrower, liquidator);

The liquidator’s repayment goes through the same priority-of-sources logic that any repay would (pool first, then P2P with matching/demotion). The seizure goes through the same withdraw flow. Liquidations are first-class but require no special code path.

The full picture

Now that you’ve added each layer, the architecture should feel inevitable. The user-facing contract Morpho.sol is a thin proxy that delegatecalls into specialized implementation contracts (the logic is too big to fit in one contract under the 24 KB code-size limit):

MORPHO CONTRACT STRUCTURE — STATE LIVES IN THE PROXYUSER-FACING PROXYMorpho.solstate lives heredelegatecallIMPLEMENTATIONPositionsManagersupply · borrow · withdraw · repayIMPLEMENTATIONInterestRatesManagerP2P index mathIMPLEMENTATIONRewardsManagerCOMP / AAVE accrualinternal callINTERNAL ENGINEMatchingEnginematch · unmatch · DLL bookkeeping
The Morpho contract structure. All modules share storage via inheritance from MorphoStorage.

The supply flow, one more time

  1. updateP2PIndexes(market) — accrue interest on stored balances.
  2. If a p2pBorrowDelta exists, use the new supply to pay it down first.
  3. Walk borrowersOnPool (largest first), promoting them to inP2P until either the supply is fully absorbed or the matching gas budget is exhausted.
  4. Any leftover supply: deposit to the pool the boring way.
  5. Update the user’s position in the sorted linked lists.

The withdraw flow, one more time

  1. updateP2PIndexes.
  2. Check the user isn’t being made undercollateralized.
  3. Withdraw from the user’s onPool first (cheap).
  4. Decrement inP2P; eat any p2pSupplyDelta (cash already parked on the pool waiting for someone to claim it).
  5. Promote a pool supplier into the freed slot (replacement).
  6. If still short, demote matched borrowers to the pool (gas-bounded).
  7. Whatever remains creates a p2pBorrowDelta; Morpho borrows it from the pool.
  8. Transfer the underlying to the user.

The summary table

ConceptWhy it exists
Two scaled balances (onPool + inP2P)Users can be partially matched.
Two index pairs (pool + P2P)Two different rates accrue to two different buckets without touching individual balances.
Sorted doubly-linked listsFast iteration in matching; O(1) move between buckets.
maxGasForMatchingCaps work so transactions can’t be DOSed by long lists.
p2pSupplyDelta, p2pBorrowDeltaTrack residual work the matching engine didn’t finish, so the index can blend rates honestly until cleared.
p2pIndexCursorPicks where in the spread the P2P rate sits.
reserveFactorDAO’s revenue share of the surplus.
Inherited collateral / oracle / liquidationNo new risk surface; composability with the pool.

Trade-offs you’ve signed up for

  • Higher gas per operation. Matching, list maintenance, and delta accounting add work beyond what the underlying pool needs. Small positions may not justify the gas premium.
  • No COMP / AAVE rewards on matched positions. Pool rewards accrue only to onPool users. P2P users trade rewards for better base rates.
  • Worst-case behavior reverts to pool rates. Under stress (mass exit, no matches available), big deltas push effective rates toward the pool. Morpho is “boost in normal times,” not “boost no matter what.”
  • Extra smart-contract surface. Strictly more code than vanilla pool usage. Heavily audited but a real consideration.

Closing thought

Every layer of Morpho is the answer to a question the previous layer couldn’t answer. Two buckets answered “what if a user is partially matched?” Indexes answered “how do we accrue without touching balances?” Linked lists answered “how do we find counterparties without iterating mappings?” Gas budgets answered “how do we bound long loops?” Deltas answered “what happens to the residual?” Cursor and reserve factor answered “where exactly is the P2P rate, and who pays for the protocol?” Inheriting risk parameters answered “do we have to redo collateral and liquidation?”

That’s the whole optimizer. Re-read the questions in order — if you can recite the answer to each one without the page, you understand Morpho-Compound and Morpho-AaveV2 well enough to read the source.