How to Audit an Aave-Fork Lending Protocol: The Complete Playbook
Most Aave forks die the same way: stale oracles, donation attacks on empty markets, or a re-entrant flash loan. Here's the audit checklist that would have saved Cream's $130M and Hundred Finance's $7M.
Aave and Compound forks are the most copy-pasted code in DeFi, and also the most consistently drained. The core contracts are battle-tested. The forks are not. Almost every nine-figure lending hack traces back to the *configuration layer* and the *peripheral integrations* — not the cToken or aToken logic itself.
Here's how I audit a lending fork in practice, in the order things actually break.
1. Oracle: Where Forks Go to Die
If you remember one thing: **the oracle is the protocol.** A lending market is a bet that your price feed is correct at the exact moment of liquidation. Get this wrong and nothing else matters.
The failure modes:
**Spot price from a single DEX pool.** Inverse Finance lost $15.6M in April 2022 because INV/ETH was priced from a Sushi pool that an attacker manipulated with a flash loan.
- **Illiquid LP token pricing.** Cream Finance, $130M in October 2021, used `getUnderlyingPrice` on yUSD LP shares where the attacker inflated `pricePerShare` by donating to the underlying vault.
- **Read-only reentrancy on Curve LP oracles.** `get_virtual_price()` lies during a Curve `remove_liquidity` callback. Sturdy Finance lost $800K to exactly this in June 2023.
Vulnerable pattern:
```solidity
function getUnderlyingPrice(CToken cToken) external view returns (uint) {
address pool = lpPools[address(cToken)];
// Spot reserves — manipulable in one tx
(uint112 r0, uint112 r1,) = IUniswapV2Pair(pool).getReserves();
return (uint(r1) * 1e18) / uint(r0);
}
```
Fixed pattern — Chainlink with staleness + sanity bounds, plus a reentrancy guard on Curve LP reads:
```solidity
function getUnderlyingPrice(CToken cToken) external view returns (uint) {
AggregatorV3Interface feed = feeds[address(cToken)];
(, int256 answer,, uint256 updatedAt,) = feed.latestRoundData();
require(answer > 0, "bad price");
require(block.timestamp - updatedAt < 3600, "stale");
require(uint256(answer) >= minPrice[address(cToken)], "floor");
require(uint256(answer) <= maxPrice[address(cToken)], "ceil");
return uint256(answer) * 1e10;
}
modifier curveReentrancyCheck(address pool) {
ICurvePool(pool).claim_admin_fees(); // reverts if locked
_;
}
```
For Curve LP tokens, never trust `get_virtual_price()` without forcing a state-modifying call on the pool first. That's the patch Sturdy *should* have had.
If you're shipping a fork, [run a free AI audit](https://www.cryptohawking.com/audit) before mainnet — it catches the obvious oracle and accrual bugs in minutes.
2. Interest Accrual Ordering
Every state-changing function — `mint`, `redeem`, `borrow`, `repayBorrow`, `liquidateBorrow`, `seize`, `transfer` — must call `accrueInterest()` first. Forks routinely add new functions (flash loans, leverage shortcuts, ERC-4626 wrappers) and forget.
The symptom: an attacker calls the un-accrued function to read or write balances at a stale exchange rate, then triggers accrual to realize free value.
```solidity
// VULNERABLE — leverage helper skips accrual
function leveragedMint(uint amount) external {
cToken.mint(amount);
cToken.borrow(amount / 2); // borrow at stale borrowIndex
}
// FIXED
function leveragedMint(uint amount) external {
require(cToken.accrueInterest() == 0, "accrue failed");
cToken.mint(amount);
cToken.borrow(amount / 2);
}
```
3. Empty-Market Donation Attack
The classic first-depositor inflation attack on ERC-4626 hits lending forks too. If `totalSupply == 0`, the first minter sets `exchangeRate`. They deposit 1 wei, get 1 cToken, then donate 1000e18 underlying directly to the contract. Now `exchangeRate = 1000e18`. Second depositor sends 999e18, gets zero shares due to integer truncation. Attacker redeems and steals everything.
Compound v2 mitigates this by minting initial cTokens at a fixed rate and requiring the deployer to seed each market. Many forks skip this step.
Fix: bootstrap every new market with a non-trivial deployer deposit before opening it to users, and use virtual shares (OpenZeppelin's ERC-4626 v4.9+) for any 4626-style wrapper.
4. Collateral Factor & Listing Risk
Hundred Finance lost $7M on Gnosis Chain in April 2023. The exploit: an attacker deposited a tiny amount of a low-liquidity market, manipulated the exchange rate via donation, and borrowed against the inflated collateral. Two failures stacked:
1. The market had near-zero TVL but a non-zero collateral factor.
2. The price oracle didn't cap the cToken's effective collateral value.
Audit checklist for every listed asset:
Collateral factor zero until liquidity floor met (e.g., $1M).
- Borrow caps and supply caps enforced in `Comptroller`.
- Per-asset isolation mode for anything not blue-chip.
- No collateral factor on rebasing tokens, fee-on-transfer tokens, or tokens with upgradeable logic you don't control.
5. Liquidation Incentive Math
Liquidations must remain profitable for bots across the full volatility range, but not so profitable that they cause cascading bad debt. Two failure modes:
**Liquidation incentive too low** → no bot calls `liquidateBorrow`, positions go underwater, protocol eats bad debt.
- **Close factor too high on volatile assets** → one liquidation wipes the whole position and triggers oracle slippage on the seized collateral.
Check `liquidationIncentiveMantissa` against the realistic gas + slippage cost on your chain. On L2s with cheap gas, 5% can work. On L1 during congestion, you need 8–10%.
Also verify the `seize` flow handles the case where `repayAmount * priceBorrowed > collateralBalance * priceCollateral` — the borrower has more debt than collateral and the bot must take what's available without reverting.
6. Flash Loan & Reentrancy Surface
If your fork adds Aave-style flash loans, the callback must not allow re-entering `borrow` or `liquidateBorrow` during the same tx. Compound v3's `supplyTo` + `withdraw` pattern is reentrancy-safe by design; bolted-on flash loan modules often aren't.
Also check cross-function reentrancy: can a malicious cToken (custom underlying with hooks) call back into `Comptroller.exitMarket` mid-liquidation?
What to Hand the Auditors
Before engagement, prepare: every oracle address with feed type, every listed asset's risk parameters in a spreadsheet, a list of all functions you added on top of the base fork, and the deployment script showing market bootstrap deposits. Forks fail at the seams, so the seams are what gets reviewed first.
For production deployments, a [manual audit](https://www.cryptohawking.com/audit/manual) — three business days, $5,000 in ETH/SOL/USDT — covers the oracle integration math and the listing parameter review that automated tools simply cannot reason about.
TL;DR
The Compound v2 core code is fine. Your fork is not. The bug is almost always in the oracle you wired up, the asset you listed too aggressively, or the helper function you added that forgot to accrue interest. Audit those three surfaces first and you've eliminated 90% of historical lending-fork exploits.
FAQ
Is forking Compound v2 or Aave v2 still safe in 2025?
The core contracts are safe in the sense that they've been audited dozens of times and run billions in TVL. But every exploited fork in the last four years (Cream, Hundred, Inverse, Sturdy, Midas, Onyx) ran essentially unmodified Compound v2 code. The exploits target the periphery — oracles, listed assets, helper contracts. Forking is fine; deploying a fork without auditing the oracle layer and the per-asset risk parameters is not. Treat the fork as scaffolding, not a finished product.
What oracle should I use for a lending protocol?
Chainlink for anything with a Chainlink feed, with mandatory staleness checks (typically 1 hour heartbeat) and min/max bounds to catch flash crashes. For long-tail assets without Chainlink, use a TWAP from a deep Uniswap v3 pool with a minimum 30-minute observation window — never spot reserves. For Curve LP tokens, use the official Curve LP oracle pattern with a reentrancy lock (the bug that hit Sturdy Finance). Never price LP shares from `pricePerShare` directly without sanity bounds.
How do I prevent the first-depositor inflation attack?
Three options. First, have the deployer seed every market with a meaningful initial deposit (e.g., $1000 of underlying) before opening it to users — burn those shares to a dead address so they can't be withdrawn. Second, use OpenZeppelin's ERC-4626 implementation v4.9+ which adds virtual shares and assets to make donation attacks economically infeasible. Third, enforce a minimum first-deposit size in code. Compound v2's `mint` actually uses an initial exchange rate of 0.02 to make the attack expensive, but several forks have changed this constant without understanding why.
What collateral factor is safe for a new asset?
Zero, until the market has meaningful liquidity. The standard playbook: list with collateral factor = 0 (asset can be borrowed but not used as collateral), wait for $1M+ in supply liquidity and verify the oracle behaves under stress, then raise to 50% for blue-chip and 25-40% for mid-cap. Never exceed 75% even for stablecoins — that's the Aave standard for USDC and it exists for a reason. Use supply caps and borrow caps as a second line of defense; Hundred Finance would have survived with proper caps.
Should I add flash loans to my Aave fork?
Only if you have a specific reason and budget for the additional audit surface. Flash loans dramatically expand the attack surface for oracle manipulation against your own protocol and others integrating with it. If you do add them, follow Aave v3's pattern exactly: fee taken before callback, balance check after, reentrancy lock on every other state-changing function. Test with a fork-of-fork scenario where your flash loan is used to manipulate a Curve pool you also use as an oracle source.