Compound v3 (Comet) Design
Smart contract security audit by TierZero · 2023-08-15
Independent research. This is an unsolicited security analysis published by TierZero — not a commissioned audit, and no certificate is issued. It reflects our own review of public code and on-chain events.
Findings summary
Independent research — not a commissioned audit.
What it is
Compound v3, branded Comet, redesigned Compound's lending market around a single borrowable base asset per deployment. The USDC-on-Ethereum market lets users supply USDC and earn interest, or supply a whitelisted set of collateral assets (WETH, WBTC, COMP, wstETH, and later additions) to borrow only USDC. There is no cross-collateral borrowing and no borrowing of the collateral assets themselves — collateral sits in the protocol purely to back a USDC debt position. This is a deliberate step back from Compound v2's every-asset-is-both-a-supply-and-borrow-market topology, which had made cross-asset contagion and cross-market liquidation math the dominant source of complexity and risk.
Each account holds one signed base-asset balance (positive = supplying, negative = borrowing) tracked through a principal/present-value index system similar in spirit to v2's exchange rate, plus a separate per-asset collateral ledger. Liquidation is split into two independent phases — absorb and buy — rather than the single atomic repay-and-seize call v2 used.
Threat model
The attack surface a reviewer has to reason about is narrower than a general money market, but not trivial:
- Oracle manipulation or staleness driving collateral valuation, since
isLiquidatable()and borrow capacity both key off a single price feed per asset. - Liquidation incentive miscalibration — if the discount offered on seized collateral doesn't cover gas and price risk during a fast drawdown, absorbs stop happening and bad debt accrues to reserves.
- Reserve depletion — absorb pays off the bad account's debt from protocol reserves before collateral is resold, so a wave of underwater accounts in a single block can outrun reserves.
- Governance/pause-guardian key compromise, since supply caps, collateral factors, and the pause guardian are all governance-controlled parameters that directly gate solvency.
- Interest-rate curve gaming through utilization spikes (flash-loan-driven borrow/repay) to distort rates within a block.
Because only one asset is borrowable, the blast radius of any single failure is bounded to that deployment's base asset — a WETH price incident cannot cascade into a WBTC market the way it could in a shared-pool design.
Why the design holds (key invariants & mitigations)
Invariant 1 — a position is only ever unhealthy in one dimension. Health is a single ratio: sum of (collateral balance × price × collateral factor) versus base debt. No basket math, no need to reason about which of several borrowed assets to seize against which collateral.
isLiquidatable(account) =
sum(collateral[i].balance * price(i) * liquidateCollateralFactor[i])
< borrowBalance(account) * price(base)
Invariant 2 — absorb decouples debt clearance from asset disposal.
function absorb(account):
require isLiquidatable(account)
debt = borrowBalance(account)
reserves -= debt // protocol temporarily eats the debt
seize all collateral[account] -> protocol
account.borrowBalance = 0
pay caller a liquidation incentive (points / rewards)
function buyCollateral(asset, amount):
price = oraclePrice(asset) * (1 - storeFrontDiscount)
caller pays `price * amount` in base asset -> reserves
protocol transfers `amount` of asset -> caller
This matters because it removes the requirement that a single liquidator hold enough capital to both repay debt and immediately absorb price risk on the seized collateral in one transaction. Any account can call absorb, earning a fixed incentive; disposal of the seized collateral is a separate, permissionless, Dutch-auction-style sale open to anyone with base asset. That two-step split is the single biggest structural difference from v2 and the main reason liquidations stay reliable under stress — it turns one hard problem (find a liquidator with the right capital at the right moment) into two easy ones.
Invariant 3 — exposure per collateral asset is capped. supplyCap on each collateral bounds how much of the protocol's solvency can depend on any one asset's price feed, which limits worst-case reserve drawdown from a single oracle failure.
Invariant 4 — principal/present-value conversion is monotonic and rounds against the user, not the protocol, on both the supply and borrow index — the same rounding discipline that closed several v2-era edge cases.
Where implementers get it wrong
Teams forking Comet's codebase most often break the assumption that made the design tractable in the first place. Adding a second borrowable asset without redesigning absorb() reintroduces the cross-asset seizure math Comet was built to avoid, while keeping the single-collateral-ledger data structures — a half-migration that's worse than either pure design. Other recurring mistakes: setting the liquidation discount too tight, so buyCollateral calls stop being profitable in volatile markets and seized collateral sits on the protocol's balance sheet; omitting oracle staleness/deviation checks when swapping in a custom feed; and forgetting that collateral assets must stay non-rehypothecatable — some forks add a "collateral also earns yield" feature that quietly reintroduces the exact commingling risk the single-borrowable-asset model was designed to eliminate. Formal specification of the absorb/buy invariants, the same way trading-system invariants get pinned down under Certora-style formal verification, catches most of these before deployment rather than after. The interest-accrual and allowance patterns are also worth comparing against the gasless-approval failure modes covered in our review of Permit2 and EVM sniping-bot exposure, since Comet's base-asset allowance flow shares the same signature-replay considerations.
Takeaways
Comet's single-borrowable-asset constraint is not a limitation bolted on for simplicity's sake — it's the mechanism that makes the two-phase absorb/buy liquidation design solvable without requiring liquidators to carry large, fast-moving inventories. Reviewers auditing a fork should treat any deviation from that constraint (multi-asset borrowing, collateral yield, shared reserves across deployments) as a request to re-derive the liquidation invariants from scratch, not a parameter change. This kind of design-level reasoning, distinct from a line-by-line vulnerability sweep, is exactly what we do in a smart contract audit engagement — if you're forking or extending a battle-tested design like Comet, get the invariant analysis done before the collateral list grows.
Need your contracts audited?
Manual review + tooling across TON, Solana and EVM — certificate and full report included.
Get an audit